We access and update the store concurrently. Our strategy to do that safely
is as follows.
The store entries once created are immutable. This alone simplifies matters
considerably.
Additionally, the way UnitId hashes are constructed means that if a store
entry exists already then we can assume its content is ok to reuse, rather
than having to re-recreate. This is the nix-style input hashing concept.
A consequence of this is that with a little care it is safe to race
updates against each other. Consider two independent concurrent builds that
both want to build a particular UnitId, where that entry does not yet
exist in the store. It is safe for both to build and try to install this
entry into the store provided that:
Note that because builds are not reproducible in general (nor even
necessarily ABI compatible) then it is essential that the loser abandon
their build and use the one installed by the winner, so that subsequent
packages are built against the exact package from the store rather than some
morally equivalent package that may not be ABI compatible.
Our overriding goal is that store reads be simple, cheap and not require
locking. We will derive our write-side protocol to make this possible.
The read-side protocol is simply:
Given our read-side protocol, the final step on the write side must be to
atomically rename a fully-formed store entry directory into its final
location. While this will indeed be the final step, the preparatory steps
are more complicated. The tricky aspect is that the store also contains a
number of shared package databases (one per compiler version). Our read
strategy means that by the time we install the store dir entry the package
db must already have been updated. We cannot do the package db update
as part of atomically renaming the store entry directory however. Furthermore
it is not safe to allow either package db update because the db entry
contains the ABI hash and this is not guaranteed to be deterministic. So we
must register the new package prior to the atomic dir rename. Since this
combination of steps are not atomic then we need locking.
The write-side protocol is:
Create a unique temp dir and write all store entry files into it.
Take a lock named after the UnitId in question.
Once holding the lock, check again for the existence of the final store
entry directory. If the entry exists then the process lost the race and it
must abandon, unlock and re-use the existing store entry. If the entry
does not exist then the process won the race and it can proceed.
Register the package into the package db. Note that the files are not in
their final location at this stage so registration file checks may need
to be disabled.
Atomically rename the temp dir to the final store entry location.
Release the previously-acquired lock.
Obviously this means it is possible to fail after registering but before
installing the store entry, leaving a dangling package db entry. This is not
much of a problem because this entry does not determine package existence
for cabal. It does mean however that the package db update should be insert
or replace, i.e. not failing if the db entry already exists.