File: cache.md

package info (click to toggle)
ocaml-dune 3.20.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,564 kB
  • sloc: ml: 175,178; asm: 28,570; ansic: 5,251; sh: 1,096; lisp: 625; makefile: 148; python: 125; cpp: 48; javascript: 10
file content (279 lines) | stat: -rw-r--r-- 14,271 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
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.