This document describes the proposed container format for streaming and storing collections of data in Bazaar. Initially this will be used for streaming revision data for incremental push/pull in the smart server for 0.18, but the intention is that this will be the basis for much more than just that use case.
In particular, this document currently focuses almost exclusively on the streaming case, and not the on-disk storage case. It also does not discuss the APIs used to manipulate containers and their records.
To create a low-level file format which is suitable for solving the smart server latency problem and whose layout and requirements are extendable in future versions of Bazaar, and with no requirements that the smart server does not have today.
A container is a streamable file that contains a series of records. Records may have names, and consist of bytes.
Here’s a brief description of use cases this format is intended to support.
It would be nice if we could combine multiple containers into a single stream by something no more expensive than concatenation (e.g. by omitting end/start marker pairs).
This doesn’t imply that such a combination necessarily produces a valid container (e.g. care must be taken to ensure that names are still unique in the combined container), or even a useful container. It is simply that the cost of assembling a new combined container is practically as cheap as simple concatenation.
Consider the use case of incremental push/pull, which is currently (0.16) very slow on high-latency links due to the large number of round trips. What we’d like is something like the following.
A client will make a request meaning “give me the knit contents for these revision IDs” (how the client determines which revision IDs it needs is unimportant here). In response, the server streams a single container of:
one record per file-id:revision-id knit gzip contents and graph data,
one record per inventory:revision-id knit gzip contents and graph data,
one record per revision knit gzip contents,
one record per revision signature,
end marker record.
in that order.
We want a storage format that allows lock-free writes, which suggests a format that uses rename into place, and do not modify after writing.
We want a format we can use and refine sooner rather than later. So it should be usable before the anticipated model changes for Bazaar “1.0” land, while not conflicting with those changes either.
Specifically, we’d like to have this format in Bazaar 0.18.
full texts of file versions
deltas of full texts
inventory as tree items e.g. the inventory data for 20 files
per-file graph data
Some key aspects of the described format are discussed in this section.
The overall container is not length-prefixed. Instead there is an end marker so that readers can determine when they have read the entire container. This also does not conflict with the goal of allowing single-pass writing.
The container contains a series of records. Each record is self-delimiting. Record markers are lightweight. The overhead in terms of bytes and processing for records in this container vs. the raw contents of those records is minimal.
There is a requirement that each object can be given an arbitrary name. Some version control systems address all content by the SHA-1 digest of that content, but this scheme is unsatisfactory for Bazaar’s revision objects. We can still allow addressing by SHA-1 digest for those content types where it makes sense.
Some proposed object names:
to name a revision: “
revision:revision-id”. e.g., revision:email@example.com.
to name an inventory delta: “
inventory.delta:revision-id”. e.g., inventory.delta:firstname.lastname@example.org.
It seems likely that we may want to have multiple names for an object.
This format allows that (by allowing multiple
name headers in a Bytes
Although records are in principle addressable by name, this specification alone doesn’t provide for efficient access to a particular record given its name. It is intended that separate indexes will be maintained to provide this.
It is acceptable to have records with no explicit name, if the expected use of them does not require them. For example:
a record’s content could be self-describing in the context of a particular container, or
a record could be accessed via an index based on SHA-1, or
when streaming, the first record could be treated specially.
The overhead for storing fairly short records (tens of bytes, rather than thousands or millions) is minimal. The minimum overhead is 3 bytes plus the length of the decimal representation of the length value (for a record with no name).
This describes just a basic layer for storing a simple series of “records”. This layer has no intrinsic understanding of the contents of those records.
The format is:
a container lead-in, “
Bazaar pack format 1 (introduced in 0.18)\n”,
followed by one or more records.
A record is:
a 1 byte kind marker.
0 or more bytes of record content, depending on the record type.
An End Marker record:
has a kind marker of “
no content bytes.
End Marker records signal the end of a container.
A Bytes record:
has a kind marker of “
followed by a mandatory content length 1: “number
\n”, where number is in decimal, e.g:1234
followed by zero or more optional names: “name
followed by an end of headers byte: “
followed by some bytes, exactly as many as specified by the length prefix header.
So a Bytes record is a series of lines encoding the length and names (if any) followed by a body.
For example, this is a possible Bytes record (including the kind marker):
B26 example-name1 example-name2 abcdefghijklmnopqrstuvwxyz
Names should be UTF-8 encoded strings, with no whitespace. Names should be unique within a single container, but no guarantee of uniqueness outside of the container is made by this layer. Names need to be at least one character long.
This requires that the writer of a record knows the full length of the record up front, which typically means it will need to buffer an entire record in memory. For the first version of this format this is considered to be acceptable.