This commit is contained in:
parent
aa2ff7b2f0
commit
2f3113f55d
11742 changed files with 1223940 additions and 0 deletions
5
unikernel/duniverse/dune_/doc/dev/README.md
Normal file
5
unikernel/duniverse/dune_/doc/dev/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Developer documentation
|
||||
|
||||
We use `doc/dev` for storing developer documentation, including specification,
|
||||
design and implementation notes. Anything that should be user-facing should go
|
||||
into the `doc` folder instead.
|
||||
279
unikernel/duniverse/dune_/doc/dev/cache.md
Normal file
279
unikernel/duniverse/dune_/doc/dev/cache.md
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
# Dune cache: design and implementation notes
|
||||
|
||||
This document describes main ideas behind the Dune cache as well as a few
|
||||
subtleties of the implementation. This is a working document and it will be
|
||||
updated as part of on-going development. Some of the described features are
|
||||
currently still in development or unused, in particular, here we describe
|
||||
support for two types of cache entries – artifacts and values – but
|
||||
Dune currently doesn't store any value entries in the cache.
|
||||
|
||||
The design and implementation are based on a non-trivial assumption that there
|
||||
are no hash collisions, for instance, that we will never come across two files
|
||||
with different contents but with the same content hash. While this assumption is
|
||||
common in the world of build systems and package managers, and is highly
|
||||
unlikely to be violated by chance, one can manufacture hash collisions on
|
||||
purpose, especially when using a weak hash like MD5.
|
||||
|
||||
## What we store in the cache
|
||||
|
||||
The cache stores build _artifacts_ and _values_.
|
||||
|
||||
* An _artifact_ is a file produced by a build rule. As any file, it has a name
|
||||
as well as content. Note that we treat a file's executable permission bit as
|
||||
part of its content.
|
||||
|
||||
* A _value_ is anything else produced during a build that is not written to a
|
||||
file but which is worth storing persistently between successive builds. A
|
||||
common example is a string written to the standard output by a build action.
|
||||
Such output-producing actions may run as part of a build rule or at an earlier
|
||||
stage when generating rules. Unlike artifacts, values have no names, yet they
|
||||
still have content.
|
||||
|
||||
## How we use the cache
|
||||
|
||||
The build system uses the cache to _store_ and _restore_ build artifacts and
|
||||
values. Here is a typical interaction sequence for the case of artifacts:
|
||||
|
||||
* The build system is executing a build rule and has already identified all of
|
||||
its dependencies, thereby obtaining the _rule hash_. It uses it to make a
|
||||
_restore request_ to the cache.
|
||||
|
||||
* Now there are two cases:
|
||||
|
||||
- The cache successfully restores all the artifacts of the rule, placing
|
||||
them into the build directory and returning their content hashes. The
|
||||
build system can skip building the artifacts and can use the obtained
|
||||
content hashes for deciding whether any dependent build rules need to be
|
||||
rerun. **This successful scenario is the only reason we use the cache.**
|
||||
|
||||
- The cache fails to restore the artifacts, either because of an error or
|
||||
because it doesn't have an entry for the given rule hash. In this case,
|
||||
the build system needs to build the artifacts itself. On completion, it
|
||||
makes a _store request_ to the cache, providing a list of build artifacts
|
||||
(_file names_ in the build directory) as well as their _content hashes_.
|
||||
The cache stores the artifacts and after that the build system is allowed
|
||||
to continue with the build (but not before, since the cache requires
|
||||
exclusive access to the artifacts in the build directory). There can be a
|
||||
rare situation where the store request is declined because the given entry
|
||||
is already in the cache. How could this happen? We did try to restore it
|
||||
first! The reason is that the cache can be populated concurrently by
|
||||
multiple build systems and by the distributed cache daemon, so there can
|
||||
be a race between multiple systems adding the same entry to the cache. In
|
||||
this case, the cache will perform _deduplication_ of build artifacts by
|
||||
replacing them with hard links to the copies already stored in the cache
|
||||
(this is an example where the exclusive access is needed).
|
||||
|
||||
Build values are handled similarly; the main difference is that to identify a
|
||||
cache entry we use the corresponding _action hash_ rather than the _rule hash_.
|
||||
The only difference between rule hashes and action hashes is that the former
|
||||
include the names of the produced artifacts into the hash, while the latter
|
||||
do not, since values have no names.
|
||||
|
||||
## Cache storage format
|
||||
|
||||
Let `root` stand for the cache root directory. It has three main subdirectories.
|
||||
|
||||
* `root/meta/v3` stores _metadata files_, one per each historically executed
|
||||
build rule or value-producing action. (While this is a convenient mental
|
||||
model, in reality we need to occasionally remove some outdated metadata files
|
||||
to free disk space – see the section on cache trimming.)
|
||||
<br/><br/>
|
||||
A metadata file corresponding to a build rule is named by the rule hash and
|
||||
stores file names and content hashes of all artifacts produced by the rule.
|
||||
<br/><br/>
|
||||
A metadata file corresponding to a value-producing action is named by the
|
||||
action hash and stores the hash of the resulting value.
|
||||
<br/><br/>
|
||||
It is important to guarantee that rule and action hashes do not accidentally
|
||||
overlap, which may happen if one simply hashes their in-memory representations
|
||||
because a rule and an action might happen to be represented by the same
|
||||
sequence of bytes in memory.
|
||||
|
||||
* `root/files/v3` is a storage for artifacts, where files named by content
|
||||
hashes store the matching contents. We will create hard links to these files
|
||||
from build directories and rely on the hard link count, as well as on the last
|
||||
change time as useful metrics during cache trimming.
|
||||
|
||||
* `root/values/v3` is a storage for values. As in the case of `files`, we store
|
||||
the values in the files named by their content hashes. However, these files
|
||||
will always have the hard link count equal to 1, because they do not appear
|
||||
anywhere in build directories. By storing them in a separate directory, we
|
||||
simplify the job of the cache trimmer.
|
||||
|
||||
* `root/temp` contains temporary files used for atomic file operations needed
|
||||
when adding new entries to the cache, as will be described below.
|
||||
|
||||
Note that since this document was first written, some of the above paths have
|
||||
changed due to version bumps (to `v4` and beyond).
|
||||
|
||||
## Adding entries to the cache
|
||||
|
||||
To add entries to the cache, we use the functions `store_artifacts` and
|
||||
`store_value` described in the corresponding sections below. Setting possible
|
||||
errors aside, these functions can succeed in two ways.
|
||||
|
||||
* They return `Stored` if the given entry is new and it has been successfully
|
||||
stored in the cache.
|
||||
|
||||
* They return `Already_present` if the given entry has already been present in
|
||||
the cache and can therefore be discarded. This is a rare scenario where
|
||||
multiple systems race to add the same entry, and only one of them will receive
|
||||
the glory of the `Stored` response.
|
||||
|
||||
### Atomic writing to the cache
|
||||
|
||||
As mentioned above, the cache can be modified concurrently by multiple systems,
|
||||
so to prevent collisions on individual files, we need to create new files
|
||||
atomically. To do that, we first create a temporary file in the `temp`
|
||||
directory, then create a hard link to it from the cache (this operation will
|
||||
fail if another process managed to create the cache entry earlier), and then
|
||||
unlink the temporary file.
|
||||
|
||||
From now on, whenever we say "create a file", we mean create a file atomically.
|
||||
If two systems attempt to create a file with the same name simultaneously, one
|
||||
of them will win the competition and the contents it writes will remain in the
|
||||
cache until it is deleted during cache trimming.
|
||||
|
||||
Note that it is possible for a metadata file with a given name to have multiple
|
||||
possible contents due to _non-determinism_, and the cache implementation should
|
||||
not assume otherwise.
|
||||
|
||||
### Storing artifacts
|
||||
|
||||
To store artifacts produced by a build rule, we perform the following sequence
|
||||
of steps.
|
||||
|
||||
* Create a metadata file in the `meta` directory, listing all the artifacts.
|
||||
If the file already exists (which should be a rare case), verify that it
|
||||
contains the expected list of artifacts (both file names and content hashes).
|
||||
If it doesn't, we have found a non-deterministic build rule and report an
|
||||
error.
|
||||
|
||||
* For each artifact, we store it in the `files` directory using the artifact's
|
||||
content hash as the name. In each case, there are two scenarios:
|
||||
|
||||
- If the artifact is already in the cache, we perform
|
||||
deduplication by replacing the artifact in the build directory
|
||||
with a hard link to the file stored in the cache. We assume that
|
||||
the build system will wait for `store_artifacts` to complete before
|
||||
starting any further actions that might read these artifacts from
|
||||
the build directory and thus interfere with the deduplication.
|
||||
|
||||
- Otherwise, we create a hard link to the artifact from the `files`
|
||||
directory.
|
||||
|
||||
The function returns `Already_present` if the metadata file and all of the
|
||||
artifacts were already in the cache; otherwise, it returns `Stored`.
|
||||
|
||||
### Storing values
|
||||
|
||||
Storing a value is simpler than storing artifacts because there is no need
|
||||
for deduplication. The steps are:
|
||||
|
||||
* Create a metadata file in the `meta` directory, recording the value's hash.
|
||||
If the file already exists (which should be a rare case), verify that it
|
||||
contains the same hash. If it doesn't, we have found a non-deterministic build
|
||||
action and report an error.
|
||||
|
||||
* Store the value as a file in the `values` directory using the value's hash as
|
||||
the file name. If the file is already in the cache, we don't need to do
|
||||
anything.
|
||||
|
||||
The function returns `Already_present` if the metadata file and the value were
|
||||
already in the cache; otherwise, it returns `Stored`.
|
||||
|
||||
## Restoring entries from the cache
|
||||
|
||||
To restore entries from the cache, we use the functions `restore_artifacts` and
|
||||
`restore_value` described in the corresponding sections below. Setting possible
|
||||
errors aside, these functions either fail to find the entry in the cache and
|
||||
return `Not_found_in_cache`, or succeed and return `Restored` along with some
|
||||
information about the restored entry.
|
||||
|
||||
### Restoring artifacts
|
||||
|
||||
Given a rule hash, the function `restore_artifacts` performs the following
|
||||
steps.
|
||||
|
||||
* Look up the corresponding metadata file in the `meta` directory. If it doesn't
|
||||
exist, return `Not_found_in_cache`. Otherwise, read the list of artifacts,
|
||||
i.e. the list of file names and their content hashes from the metadata file.
|
||||
|
||||
* For each artifact, lookup the content hash in the `files` directory. If it
|
||||
doesn't exist, return `Not_found_in_cache`. Otherwise: (i) delete the
|
||||
corresponding (most likely stale) file in the build directory, and then (ii)
|
||||
create a hard link with the same name, pointing to the file in the cache.
|
||||
|
||||
If the above succeeds for every artifact in the list, the function returns
|
||||
`Restored` along with the obtained list of file name and content hash pairs.
|
||||
|
||||
### Restoring values
|
||||
|
||||
Given an action hash, the function `restore_value` performs the following steps.
|
||||
|
||||
* Look up the corresponding metadata file in the `meta` directory. If it doesn't
|
||||
exist, return `Not_found_in_cache`. Otherwise, read the hash of the value.
|
||||
|
||||
* Look up the hash in the `values` directory. If it doesn't exist, return
|
||||
`Not_found_in_cache`. Otherwise, return `Restored` along with the value read
|
||||
from the stored file.
|
||||
|
||||
## Trimming the cache
|
||||
|
||||
Storing all historically produced artifacts and values is infeasible, so the
|
||||
cache needs to be regularly trimmed. The current trimming algorithm performs the
|
||||
following steps.
|
||||
|
||||
* Scan the `files` directory to find all currently unused artifact entries. An
|
||||
artifact is _unused_ if its hard link count is equal to 1. There is no point in
|
||||
trimming other entries, since they appear in at least one build directory. In
|
||||
fact, trimming them is potentially harmful because if the same entries were to
|
||||
be added to the cache again from a new directory, we would have been unable to
|
||||
perform the deduplication, thus losing some sharing opportunities.
|
||||
|
||||
* Scan the `values` directory to find all value entries. We have no information
|
||||
about their current usage, so we conservatively allow all of them to be
|
||||
trimmed and recomputed in the next build if needed.
|
||||
|
||||
* Sort the entries according to the following criteria:
|
||||
|
||||
- Type: artifacts precede values in the trimming list since artifacts are
|
||||
generally larger and we know for sure that they are unused;
|
||||
|
||||
- The time of last change: entries that became unused more recently go later
|
||||
in the list.
|
||||
|
||||
* Traverse the list and delete the corresponding entries until the trimming goal
|
||||
has been met. Right before deleting an artifact entry, double check that its
|
||||
hard link count is still equal to 1. A build system running concurrently might
|
||||
have created a hard link to it after we collected the information, so deleting
|
||||
this file from the cache could lead to a loss of sharing between different
|
||||
build directories.
|
||||
|
||||
* Finally, traverse the `meta` directory and remove all _broken_ metadata files,
|
||||
i.e. the files that refer to content hashes with no corresponding entries in
|
||||
the `files` and `values` directories. This step does not need to be done on
|
||||
every trimming. It is expensive but metadata files are generally small and
|
||||
there is no harm in keeping broken metadata files in the cache. In fact, the
|
||||
information contained in broken metadata files can be utilised by the build
|
||||
system for so-called _shallow builds_ where intermediate build artifacts are
|
||||
not materialised on the disk and it is sufficient to only know their hashes,
|
||||
which are listed in the metadata files.
|
||||
|
||||
To enable more sophisticated trimming strategies, we could augment the metadata
|
||||
stored in the cache with information about the _cost_ of producing cache
|
||||
entries, i.e. the time it would take to execute the corresponding rule or action
|
||||
to restore the entry if needed. For deterministic build rules, we can do _local
|
||||
cost reasoning_, i.e. we do not need to take the cost of rebuilding their
|
||||
dependents into account, since such rebuilding would be unnecessary due to the
|
||||
early cut-off optimisation.
|
||||
|
||||
Another promising idea is to add support for incremental cache trimming where
|
||||
the build system informs the cache that a previously added entry has become
|
||||
obsolete, letting the cache trim it early if it meets the trimming criteria.
|
||||
|
||||
### Interaction with the previous cache versions
|
||||
|
||||
Note also that as the new cache format evolves further and we, for example, move
|
||||
from `files/v3` to `files/v4`, the cache trimmer will need to evolve too, to be
|
||||
able to cope with entries of all currently supported versions.
|
||||
64
unikernel/duniverse/dune_/doc/dev/directory-targets.md
Normal file
64
unikernel/duniverse/dune_/doc/dev/directory-targets.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Directory targets
|
||||
|
||||
A **directory target** corresponds to a file tree rooted at a specified root
|
||||
directory, for example, `docs/*`. Typical examples of rules producing directory
|
||||
targets are: unpacking an archive, running `make` in a vendored package, and
|
||||
building files with non-deterministic names (e.g. including the current date).
|
||||
|
||||
## Declaring a directory target
|
||||
|
||||
To declare a directory target `docs/*`, use the syntax `(target (dir docs))` in
|
||||
a rule stanza. The corresponding rule should create the directory `docs` and is
|
||||
free to populate it with an arbitrary number of files and/or subdirectories.
|
||||
|
||||
Like file targets, directory targets can be promoted to the source tree by
|
||||
adding `(mode promote)` to the rule stanza.
|
||||
|
||||
## Depending on a directory target
|
||||
|
||||
There are two ways to depend on a directory target:
|
||||
|
||||
* An **opaque dependency** on the whole file tree `docs/*`. Opaque dependencies
|
||||
are invalidated if the contents of the tree is changed in any way. To declare
|
||||
an opaque dependency on `docs/*`, use the syntax `(dep (dir docs))` in a rule
|
||||
stanza.
|
||||
|
||||
* A **projection dependency** on a specific file in the tree, e.g.
|
||||
`docs/html/index.html`. A projection dependency is declared using the standard
|
||||
syntax `(dep docs/html/index.html)` and works like a dependency on a normal
|
||||
file target. For example, if the `docs/*` directory is rebuilt and only
|
||||
`docs/html/logo.png` is modified, then the dependency on `docs/html/index.html`
|
||||
is considered to be up-to-date. Note that it is easy to make a mistake with
|
||||
such projection dependencies, for example, by forgetting that `index.html`
|
||||
actually does include the image `docs/html/logo.png`. In such cases,
|
||||
sandboxing will help since only the requested projection dependencies will be
|
||||
available in the sandbox (i.e., not the whole directory target).
|
||||
|
||||
## Building a directory target
|
||||
|
||||
Users can request building whole directory targets or individual files via
|
||||
`dune build docs` and `dune build docs/html/index.html` commands.
|
||||
|
||||
## Current limitations
|
||||
|
||||
* It is not allowed to have two rules with the same directory target. That is,
|
||||
like file targets, directory targets are **exclusive** (but see _shared
|
||||
directory targets_ below).
|
||||
|
||||
* Directory targets cannot have nested file or directory targets, i.e. other
|
||||
rules are not allowed to declare targets within the file tree of a directory
|
||||
target.
|
||||
|
||||
## Possible future extensions
|
||||
|
||||
Here are some possible extensions to consider:
|
||||
|
||||
* **Opaque directory targets**: a rule may declare that its directory target is
|
||||
opaque, in which case projection dependencies on its content will be
|
||||
disallowed. One can also consider only partially opaque directory targets,
|
||||
where the contents of the directory is only partially visible.
|
||||
|
||||
* **Shared directory targets**: we can allow multiple rules to write to the same
|
||||
directory target, as long as they do not write to the same files. In this
|
||||
case, depending on a directory target would mean depending on all of the rules
|
||||
that declare it as a target.
|
||||
106
unikernel/duniverse/dune_/doc/dev/rev-store.md
Normal file
106
unikernel/duniverse/dune_/doc/dev/rev-store.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
The Revision Store
|
||||
==================
|
||||
|
||||
The revision store is the place where Git data that is relevant to the Dune
|
||||
package management is cached.
|
||||
|
||||
The Concepts
|
||||
------------
|
||||
|
||||
The revision store uses Git in the way of its original slogan, as a
|
||||
content-addressable file system. A lot of data (code) and meta-data (opam files)
|
||||
is stored in Git repositories that are often forked from each other, hence to
|
||||
save space Dune has a Git object cache.
|
||||
|
||||
Git is implemented as a way to store revisions and being able to address them.
|
||||
However, these revisions do not have to have a common ancestor and given Git
|
||||
uses SHA1 hashes for addressing it is possible to join multiple repositories in
|
||||
one single Git repository without clashing. A fairly common usecase outside of
|
||||
Dune are `gh-pages` branches to serve documentation on Github, which do not
|
||||
share a common parent with the main branch of the repository.
|
||||
|
||||
The revision store exploits this feature by putting all revisions of all
|
||||
repositories into one single large repository to take advantage of caching
|
||||
effects.
|
||||
|
||||
The Advantages
|
||||
--------------
|
||||
|
||||
This way of organizing means that all revisions shared between multiple forks
|
||||
of a Git repository can reuse the same Git objects that are in common and don't
|
||||
need to download them nor store them again. Updating repositories can be done
|
||||
incrementally, as Git knows which revisions are available locally and which
|
||||
ones need to be fetched.
|
||||
|
||||
It is also possible to refer to previous states easily as the commits are part
|
||||
of a Git repo and checking out an older version is as simple as checking out
|
||||
the current version of the files.
|
||||
|
||||
Considerations and Compromises
|
||||
------------------------------
|
||||
|
||||
An important consideration was that the management of the revision store should
|
||||
be entirely transparent to the user, they should not need to do any steps to
|
||||
create nor maintain it. It should get created automatically if needed and all
|
||||
the steps that are necessary to keep it updated should happen in the
|
||||
background. The store should always work like a cache that can be discarded
|
||||
safely without causing data loss.
|
||||
|
||||
The revision store should always give out the most recent version of data,
|
||||
unless explicitly instructed otherwise. This means that :
|
||||
|
||||
* If only a Git source is specified, then the revision store will
|
||||
automatically get the newest revision
|
||||
* If the source specifies a tag or branch, then the revision store will
|
||||
automatically update to the newest revision
|
||||
* It a revision is specified, the revision store will only update if the
|
||||
revision is not yet cached, otherwise the cached version can be used
|
||||
|
||||
The final consideration means that an offline usage is possible if all
|
||||
repositories specified are specified with their hash.
|
||||
|
||||
Due to the fact that the revision store is a Git repository it means that the
|
||||
data sources that can be added to it also have to be available via Git. This
|
||||
means that adding repositories that use different version control systems
|
||||
aren't supported at the moment nor are plain HTTP sources supported.
|
||||
|
||||
Support for other kinds of VCSes is a possible extension by replicating similar
|
||||
concepts with other version control systems, provided they allow for similar
|
||||
flexibility as the Git way of storing revisions. However at the moment most
|
||||
users have settled on using Git, hence this version should be able to
|
||||
accommodate the needs for most users.
|
||||
|
||||
Another compromise is that old repositories with long histories and large sizes
|
||||
have to be cloned before use, thus increasing the size of the initial download
|
||||
compared to the same metadata downloaded as a compressed tarball. Despite Git
|
||||
compressing objects, the history of the repositories to be added does increase
|
||||
the overhead.
|
||||
|
||||
A solution to this could be shallow clones which only contain the latest
|
||||
revisions, however these have [shown to be
|
||||
problematic](https://blog.cocoapods.org/Master-Spec-Repo-Rate-Limiting-Post-Mortem/)
|
||||
thus for time being we are fetching the complete histories.
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
|
||||
This section describes the current way the revision store is implemented.
|
||||
|
||||
As the revision store is not project specific, it is stored in the user's cache
|
||||
directory (using the [freedesktop.org](https://www.freedesktop.org/wiki/)
|
||||
specifications, the directory specified by `XDG_CACHE_HOME`), with all dune
|
||||
instances sharing one single revision store.
|
||||
|
||||
The revision store itself is a `bare` Git repository without a worktree. This
|
||||
is because all repositories in the revision store are equal and checking out
|
||||
one particular revision would be a waste of disk space as the Git tooling can
|
||||
be used to construct any revisions out of the bare repository anyway.
|
||||
|
||||
Thus every source that is added to the revision store as a remote that tracks
|
||||
the default branch (or, if a branch is specified explicitly, then that
|
||||
branch) and fetched, thus storing the required revisions in the revision store.
|
||||
|
||||
The implementation of these features is a mix of calling the `git` binary and
|
||||
implementing parts in OCaml. This means that the `git` binary is required on
|
||||
the system. Possible future improvements could be using
|
||||
[ocaml-git](https://github.com/mirage/ocaml-git) to avoid the dependency.
|
||||
180
unikernel/duniverse/dune_/doc/dev/rpc-versioning.md
Normal file
180
unikernel/duniverse/dune_/doc/dev/rpc-versioning.md
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
# Runtime RPC versioning implementation notes
|
||||
|
||||
This document describes the versioning protocol used to ensure two-way
|
||||
compatibility between different versions of the API.
|
||||
|
||||
The approach is loosely inspired by the `Both_converts` model used by
|
||||
[Versioned_rpc](https://ocaml.janestreet.com/ocaml-core/latest/doc/async_rpc_kernel/Async_rpc_kernel/index.html#module-Versioned_rpc)
|
||||
in `Async`, in which both parties maintain a "menu" of supported RPC
|
||||
versions, which is used to negotiate a common protocol for each
|
||||
method.
|
||||
|
||||
This is a working document and will be updated as the design evolves.
|
||||
|
||||
## Terms
|
||||
|
||||
- A **procedure** is a common term encompassing *notifications*
|
||||
(one-way messages) and *requests* (a communication to which
|
||||
a response is expected).
|
||||
|
||||
- A **model** is the logical payload type for one direction of
|
||||
a procedure. Note that this is a per-actor entity; the ultimate
|
||||
goal of runtime RPC versioning is to allow clients and servers to
|
||||
disagree on a model type without preventing them from interacting.
|
||||
|
||||
- A **wire type** for a procedure is the logical type sent "over the
|
||||
wire" for one direction of a procedure. The end result of
|
||||
negotiating a version for a procedure is to select a wire type
|
||||
known to both the client and the server. Typically, the older of
|
||||
the two model types will be chosen as the wire type,
|
||||
|
||||
- A **generation** of a procedure is the set of wire types
|
||||
corresponding to each direction of a procedure, along with the
|
||||
de/serialisation logic and upgrade and downgrade functions
|
||||
transforming the wire types to the model types and vice versa.
|
||||
Each generation is associated with a *version number*, which
|
||||
should be unique within a procedure.
|
||||
|
||||
- The **menu** is a mapping from method names to the particular
|
||||
generations that will be used for each procedure for a particular
|
||||
session. In the source code, this term is overloaded to also refer
|
||||
to a mapping from method names to *all known* generations of
|
||||
a procedure.
|
||||
|
||||
- The **declaration** of a procedure lists its model types and all
|
||||
known generations, along with its method name. Multiple
|
||||
declarations of the same procedure is allowed, so long as they do
|
||||
not overlap version numbers.
|
||||
|
||||
- The **implementation** of a declaration is the actual behavior of
|
||||
a procedure, which acts on the model types. Typically, this will
|
||||
be on the server, but in the future there may also be a use for
|
||||
server-to-client requests. This document is not concerned with the
|
||||
internals of any given implementation, only whether such an
|
||||
implementation exists at all.
|
||||
|
||||
## Background
|
||||
|
||||
Previously, there was no distinction between model and wire types.
|
||||
This meant that any change to a model type required both build servers
|
||||
and clients to upgrade in lockstep, as otherwise the receiver would be
|
||||
unable to deserialize the payload of a procedure.
|
||||
|
||||
Unfortunately, most lighter-weight solutions (such as modifying the
|
||||
de/serialization logic to be resilient to, e.g., extra/missing fields
|
||||
or variants in types) are insufficient. Early designs of the
|
||||
diagnostic API, for example, reported targets as strings, but was
|
||||
changed to give structured information instead.
|
||||
|
||||
Similarly, requirements like "the client must always be older than the
|
||||
server" (or the reverse) don't work in environments like Jane Street,
|
||||
where the same editor plugin must be able to interact seamlessly with
|
||||
multiple iterations of Dune (which may be older or newer than the
|
||||
editor plugin itself).
|
||||
|
||||
The main goal of the system, then, is to ensure that both the server
|
||||
and client applications can be programmed against the current model
|
||||
types for each procedure, with all backwards- or forwards- conversions
|
||||
happening under the hood.
|
||||
|
||||
## Protocol
|
||||
|
||||
At session initialization time, the client will first send an
|
||||
initialization request to the server containing a single version
|
||||
number corresponding to the overall RPC version the client will use.
|
||||
If this number is determined to be versioning-compatible (see
|
||||
[Session versioning](#session-versioning)), the server will respond
|
||||
with a token instructing the client to initiate version negotiation.
|
||||
Otherwise, the server will respond with an error.
|
||||
|
||||
Upon receiving this token, the client will initiate version
|
||||
negotiation by sending a list of `(method-name, generations)`
|
||||
pairs, where `method-name` is the name of each declared procedure, and
|
||||
`generations` is the list of version numbers for that procedure's
|
||||
generations.
|
||||
|
||||
Upon receiving a list of supported versions from the client, the
|
||||
server will compare it to its list of *implemented* versions,
|
||||
selecting the greatest common generation for each procedure. If the
|
||||
client and server do not share any generations for a procedure, it is
|
||||
omitted entirely. If there is at least one method for which a common
|
||||
version exists, then the server responds with a list of `(method-name,
|
||||
selected-version)` pairs, where `selected-version` is the version
|
||||
number of the greatest common generation. This list is then used by
|
||||
both parties to construct the version menu. Otherwise, if there are no
|
||||
common versions for any methods, an error is returned to the client
|
||||
and the session is invalidated.
|
||||
|
||||
Note that we do not currently require declared/implemented versions to
|
||||
span a contiguous range of version numbers. This can have a few uses,
|
||||
such as preventing clients from using a known-bugged generation of
|
||||
a procedure.
|
||||
|
||||
When executing a procedure, the sender first looks up the correct
|
||||
generation in the menu (see [Error handling](#error-handling)), and
|
||||
downgrades the payload from the sender-side model type to the wire
|
||||
type. Upon receipt, the server performs the same lookup to deserialize,
|
||||
then upgrade the payload to the receiver-side model type, then the
|
||||
procedure implementation is performed, producing a response in the
|
||||
case of requests. If necessary, the same transformations are then
|
||||
performed in reverse, sending the value back to the sender, completing
|
||||
the procedure.
|
||||
|
||||
Barring strange circumstances (such as a client declaring a generation
|
||||
with a newer version number than the type exposed in `dune_rpc.mli`), it
|
||||
is always the case that the transformation from wire to model types will
|
||||
be the identity function on the side that is older.
|
||||
|
||||
## Miscellaneous implementation notes
|
||||
|
||||
### Session versioning
|
||||
|
||||
In addition to version numbers existing for each procedure version,
|
||||
there are two further version numbers associated with the session as
|
||||
a whole which are sent as part of session initialization.
|
||||
|
||||
The first is the version of Dune each side purports to be as
|
||||
a `MAJOR.MINOR` number (serialized as an `int * int` pair). This is
|
||||
not currently checked.
|
||||
|
||||
Next is a version of the initial handshake protocol to be used. This
|
||||
takes the form of a single `int`. In the future, if the initial
|
||||
negotiation protocol changes, this value can be adjusted and checked to
|
||||
account for this.
|
||||
|
||||
### Error handling
|
||||
|
||||
Handling of versioning errors has become more complex, as we need to
|
||||
distinguish between "no such method exists" and "the server and client
|
||||
do not share any common generations for this method". Secondly, this
|
||||
means that the initiation of a procedure can now fail, which
|
||||
complicates one-way communications (for example, the server must
|
||||
swallow errors and clients must be upgraded to handle version errors
|
||||
on notifications, which were previously infallible).
|
||||
|
||||
Finally, the versioning protocol itself must be either versioned
|
||||
separately or stabilised (see [Session versioning](#session-versioning)).
|
||||
|
||||
### Tweaks
|
||||
|
||||
- We currently send the entire version menu from client to server and
|
||||
back twice, once for the client to inform the server of all
|
||||
supported versions, and again for the server to inform the client
|
||||
of the common versions. This can lead to large messages being
|
||||
passed at session initialization, which may become a performance
|
||||
bottleneck.
|
||||
|
||||
- The size of the version negotiation messages is proportional to
|
||||
the number of all known generations for all procedures, which
|
||||
can be approximated by `number-of-procedures` times
|
||||
`number-of-supported-generations`. In practice, I do not
|
||||
expect this number to be large (I would be surprised if this
|
||||
number is ever on the order of 100).
|
||||
|
||||
- One alternative is to perform per-procedure negotiation, where
|
||||
the initiator of a procedure first sends its known version
|
||||
ranges, the recipient sends the selected version (or an
|
||||
error), then the procedure proceeds as before. This approach
|
||||
trades startup and lookup overhead for a constant
|
||||
per-communication overhead. It also makes distinguishing "no
|
||||
such method exists" and "no common versions" simpler.
|
||||
176
unikernel/duniverse/dune_/doc/dev/rule-production.md
Normal file
176
unikernel/duniverse/dune_/doc/dev/rule-production.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# Rule production
|
||||
|
||||
This document describes how rule production works in Dune. It was originally
|
||||
written by Jérémie Dimino as part of the
|
||||
[streaming RFC](https://github.com/ocaml/dune/pull/5251), but moved
|
||||
into the dev documentation as it provides a great overview on how this part of
|
||||
Dune works at present.
|
||||
|
||||
## How does rule production works?
|
||||
|
||||
### `Dune_engine.Load_rules`
|
||||
|
||||
The production of rules is driven by the module `Load_rules` in the
|
||||
`dune_engine` library. This library is the build system core of
|
||||
Dune. It is meant as a general purpose library for writing build
|
||||
systems, and the Dune software is built on top of it. In theory,
|
||||
`dune_engine` shouldn't know about `dune` or `dune-project`
|
||||
files. However, for historical reason this is not the case yet and
|
||||
`dune_engine` still knows some things about them.
|
||||
|
||||
As we work on Dune, we expect that `dune_engine` will become more and
|
||||
more agnostic. Even though it is not completely agnostic, we have
|
||||
successfully been using it to build Jane Street code base, using the
|
||||
Jane Street rules on top of this core. So it's already more general
|
||||
than Dune itself.
|
||||
|
||||
For the purpose of this design doc, we will treat `dune_engine` as a
|
||||
completely general library that doesn't know about `dune` files.
|
||||
|
||||
The main feature of `Load_rules` is the `Load_rules.load_dir` function:
|
||||
|
||||
```ocaml
|
||||
val load_dir : dir:Path.t -> Loaded.t Memo.t
|
||||
```
|
||||
|
||||
`Loaded.t` represents a "loaded" set of rules for a particular
|
||||
directory. It can also be thought as a "compiled" set of rules. A
|
||||
`Loaded.t` contains all the rules in existence that produce targets in
|
||||
`dir`. For instance, given a `Loaded.t` we can figure out all the
|
||||
files that would be produced in `dir` if we were building everything
|
||||
that could be built. While `dir` is a build directory, this also
|
||||
includes files present in the source tree. This is because
|
||||
`Load_rules.load_dir` implicitly adds copy rules for all source files
|
||||
present in the source directory that correspond to the build directory
|
||||
`dir`. For instance, if `dir` is `_build/default/src`, Dune will
|
||||
implicitly add rules to copy files in `src` to `_build/default/src`.
|
||||
Except for rules that have the special `promote` or `fallback` modes.
|
||||
|
||||
This is in fact how Dune evaluates globs during the build. Indeed,
|
||||
when writing `dune` files we work in an imaginary world where both the
|
||||
source files and the generated files are present. So when we write
|
||||
`(deps (glob_files *.txt))`, this `*.txt` denotes both `.txt` files
|
||||
that are present on disk in the source tree but also as the ones that
|
||||
can be generated by the build.
|
||||
|
||||
In practice, to evaluate `(glob_files *.txt)` in directory `d`, Dune
|
||||
calls `Load_rules.load_dir ~dir:d` and filter the list of files that can
|
||||
be built. Similarly, when Dune needs to build a file
|
||||
`_build/default/src/x`, it first calls `Load_rules.load_dir` with
|
||||
`_build/default/src` and then looks up a rule that has `x` has
|
||||
target in the returned `Loaded.t`. The `Load_rules.load_dir` is
|
||||
memoised, so it can be called multiple times during the build without
|
||||
guilt.
|
||||
|
||||
While `Load_rules` is responsible for driving the production of rules,
|
||||
it is part of `dune_engine` which doesn't know about `dune` files and
|
||||
doesn't know about OCaml libraries or OCaml compilation in general. So
|
||||
it is not responsible for actually producing the build rules that
|
||||
allow to build Dune projects. Instead, `Load_rules` defers the actual
|
||||
production of rules to a callback that it obtains via
|
||||
`Build_config`. Inside Dune, this callback is implemented by the
|
||||
`Gen_rules` module inside the `dune_rules` library. `dune_rules` is
|
||||
the library that is responsible for parsing, interpreting and
|
||||
compiling `dune` files down to low-level build rules.
|
||||
|
||||
### `Dune_rules.Gen_rules`
|
||||
|
||||
The entry of `Dune_rules.Gen_rules` is the `gen_rules` function. Its
|
||||
API looks like:
|
||||
|
||||
```ocaml
|
||||
val gen_rules :
|
||||
Build_config.Context_or_install.t ->
|
||||
dir:Path.Build.t ->
|
||||
string list ->
|
||||
Build_config.gen_rules_result Memo.t
|
||||
```
|
||||
|
||||
Where `Build_config.gen_rules_result` is, in most cases —when the value
|
||||
returned is `Build_config.Rules _`—, a "raw" set of rules. Raw in the sense
|
||||
that there is no overlap checks or any other checks. During the rule
|
||||
production phase, we merely accumulate a set of rules that is later
|
||||
processed. The API of `gen_rules` is in fact a bit more complex, but
|
||||
the above definition is enough for the purpose of this document.
|
||||
|
||||
The first thing `gen_rules` does is analyse the directory it is
|
||||
given. If the directory corresponds to a source directory with a `dune`
|
||||
file, `gen_rules` will dispatch the call to the part of `dune_rules`
|
||||
that parses and interprets the `dune` file. This is the simplest case,
|
||||
but even in this case there are some things worth mentioning.
|
||||
|
||||
For instance, when compiling an OCaml library dune stores the
|
||||
artifacts for the library in generated dot-directories. For instance,
|
||||
the cmi files for library `foo` living in source directory `src` will
|
||||
end up in `_build/default/src/.foo.objs/byte`. We could produce these
|
||||
rules when `gen_rules` is called with directory
|
||||
`_build/default/src/.foo.objs/byte`, however that would spread out the
|
||||
logic for interpreting `library` stanzas. It is much simpler to
|
||||
produce all the build rules corresponding to a `library` stanza in one
|
||||
go. This is what is happening at the moment: when called with
|
||||
directory `_build/default/src`, `gen_rules` will not only produce
|
||||
rules for this directory but will also produce rules for
|
||||
`_build/default/src/.foo.objs/byte` and various other directories.
|
||||
|
||||
`Load_rules` doesn't know anything about this. And in particular, it
|
||||
doesn't know that it is the `gen_rules` call for directory
|
||||
`_build/default/src/` that will produce the rules for the dot
|
||||
subdirectories. When `Load_rules` loads the rules for the
|
||||
`.../.foo.objs/byte` sub-directory, it simply calls `gen_rules` with
|
||||
this directory. It is `gen_rules` that "redirects" the call to the
|
||||
`_build/default/src` directory by calling
|
||||
`Load_rules.load_dir_and_produce_its_rules`. This function simply
|
||||
calls `Load_rules.load_dir` and re-emits all the raw rules that were
|
||||
returned by the corresponding `gen_rules` call.
|
||||
|
||||
This works because `Load_rules.load_dir` accepts the facts that
|
||||
`gen_rules` produces rules for many directory at once. It simply
|
||||
filters out the result. But for things to behave well, the unwritten
|
||||
following invariant must hold: `gen_rules ~dir:d` is allowed to
|
||||
generate rules for directory `d'` iff `gen_rules ~dir:d'` emits a call
|
||||
to `Load_rules.load_dir ~dir:d`.
|
||||
|
||||
This scenario happens in a number of cases. All these cases share a
|
||||
common pattern: the redirections are always to an ancestor
|
||||
directory. At the moment, there is one exception to this pattern in
|
||||
the odoc rules, however it is easy to remove.
|
||||
|
||||
Finally, the `copy_files` stanza creates another form of dependency
|
||||
between directory. In order to calculate the targets produced by
|
||||
`copy_files`, which needs to be known at rule production time, we need
|
||||
to evaluate the glob given to `copy_files`. Which requires doing a
|
||||
call to `Load_rules.load_dir` as previously described. Contrary to the
|
||||
other form of dependency we just describe, this ones can go from any
|
||||
directory to any other directory. For instance, the following stanza
|
||||
in `src/dune`:
|
||||
|
||||
```
|
||||
(copy_files foo/*.txt)
|
||||
```
|
||||
|
||||
would create a dependency from `_build/default/src` to
|
||||
`_build_default/src/foo`.
|
||||
|
||||
So in the end, if we were looking at the internal computation graph of
|
||||
Dune and narrowing it to just the calls to `Load_rules.load_dir`, we
|
||||
would see a graph with many edges going from a directory to one of its
|
||||
ancestor. These would mostly be between generated dot-subdirectories
|
||||
and their first ancestor that has a corresponding directory in the
|
||||
source tree. Plus a few other arbitrary ones for each `copy_files`
|
||||
stanza.
|
||||
|
||||
## Directory targets
|
||||
|
||||
Before directory targets, answering the question "what rules produces
|
||||
file X?" was easy. Dune would just call `Load_rules.load_dir` and
|
||||
lookup `X` in the result. With directory targets, things are a bit
|
||||
more complicated. Indeed, `X` might also be produced by a directory
|
||||
target in an ancestor directory. This means that `Load_rules.load_dir`
|
||||
now need to look in parent directories as well, which introduce more
|
||||
dependencies from directories to their parents and can create cycles
|
||||
because of `copy_files` stanza that create dependencies in the other
|
||||
direction.
|
||||
|
||||
At a result, some combinations of `copy_files` and directory targets
|
||||
don't produce the expected result. This is documented in the test
|
||||
suite.
|
||||
82
unikernel/duniverse/dune_/doc/dev/rule-streaming.md
Normal file
82
unikernel/duniverse/dune_/doc/dev/rule-streaming.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Rule streaming
|
||||
|
||||
This document describes a new design for the production of build rules
|
||||
in Dune. The new design aims to be more natural, easier to reason
|
||||
about and to make existing features work well with newer ones such as
|
||||
directory targets.
|
||||
|
||||
It was originally written by Jérémie Dimino as part of the
|
||||
[streaming RFC](https://github.com/ocaml/dune/pull/5251), and later on moved
|
||||
into the dev documentation.
|
||||
|
||||
|
||||
## Problem
|
||||
|
||||
The [rule production](./rule-production.md) document exposes a concrete problem
|
||||
with directory targets, but there is also a general sense of messiness in the
|
||||
way things work. Generating rules for multiple directories at once is
|
||||
natural, but the current encoding is odd.
|
||||
|
||||
## Proposal
|
||||
|
||||
The proposal is to add the following rule: `gen_rules ~dir` is allowed
|
||||
to produced rules in `dir` or any of its descendant only. It is not
|
||||
allowed to produce rules anywhere else.
|
||||
|
||||
`Load_rules.load_dir ~dir` will then always call itself recursively on
|
||||
the parent of `dir` and take the union of the rules produced by
|
||||
`gen_rules` for `dir` and the ones produced by the recursive
|
||||
call. `gen_rules` will no longer have to redirect a call via
|
||||
`Load_rules.load_dir_and_produce_its_rules`, which we would simply
|
||||
remove.
|
||||
|
||||
This introduces a cycle with all `copy_files` stanza that copy files
|
||||
from a sub-directory. We propose the break this cycle by introducing
|
||||
laziness in the rule production code.
|
||||
|
||||
### Generating rules with a mask
|
||||
|
||||
The idea is that when we produce rules, we will produce rules under
|
||||
a current active "mask" that tells us where we are allowed to generate
|
||||
files or directories. Trying to produce a rule with targets not
|
||||
matched by this mask will be a runtime error.
|
||||
|
||||
When entering `gen_rules ~dir`, the initial mask will be: "any files
|
||||
and directories that is a descendant of directory `dir`".
|
||||
|
||||
We can then narrow the mask to a sub-mask:
|
||||
|
||||
```ocaml
|
||||
val narrow : Target_mask.t -> unit Memo.t -> unit Memo.t
|
||||
```
|
||||
|
||||
With `narrow mask m`, `m` would only be allowed to produce rules whose
|
||||
target are matched by the intersection of `mask` and the current
|
||||
mask. `m` wouldn't be evaluated eagerly. Instead, `gen_rules` would
|
||||
now return a set of direct rules as well as a list of
|
||||
`(Target_mask.t * unit Memo.t)`. Let's call such a pair a
|
||||
suspension. A suspension can be forced by evaluation its second
|
||||
component. Doing so will yield a list of rules matched by the mask and
|
||||
a new list of suspension.
|
||||
|
||||
### Staged rules loading
|
||||
|
||||
The next step is to stage `Load_rules.load_dir`. In addition to taking
|
||||
a directory, `load_dir` will now also take a mask and will return the
|
||||
set of rules for this mask. To do that, it might need to force a bunch of
|
||||
suspensions recursively.
|
||||
|
||||
|
||||
### How does that help?
|
||||
|
||||
We will put `copy_rules` under a `narrow <only file targets in current
|
||||
dir>`. In order to determine if a directory is part of a directory
|
||||
target in an ancestor directory, we wouldn't need to force this
|
||||
suspension.
|
||||
|
||||
### Difficulties
|
||||
|
||||
Interpreting a `library` stanza requires knowing the set of `.ml`
|
||||
files in the current directory. Knowing this requires interpreting
|
||||
`copy_files` in the current directory. So the interpretation of
|
||||
`library` stanzas will need to go under a `narrow` as well.
|
||||
Loading…
Add table
Add a link
Reference in a new issue