nova-nix-0.9.0.0: src/Nix/Store/Handle.hs
-- | The open-store handle: the store directory, its database, and the
-- handle's lease on the store-wide garbage-collector lock.
--
-- == The lease
--
-- Every open 'Store' holds @.nova-nix\/gc.lock@ shared from 'openStore'
-- to 'closeStore'. A collector needs that lock exclusively, so a
-- collection runs only while no other process has the store open, and a
-- process opening the store while one runs waits for it to finish.
-- 'withCollectorLock' is the one place a handle trades its shared lease
-- for the exclusive lock and back; "Nix.Store.GC" states why this is the
-- protocol and what the per-path locks cannot express.
--
-- The lease is this handle's own descriptor, and the OS treats each
-- descriptor as a separate holder: a process that opens two handles on
-- one store directory and collects through either waits forever on its
-- own other lease. One handle per store per process is the rule.
--
-- == The directory's spelling
--
-- The database keys every row by the store directory's spelling
-- ("Nix.Store.DB"), so 'openStore' keys the handle by one canonical
-- spelling ('canonicalStoreDir') whatever the caller typed: relative or
-- absolute, with or without a trailing separator, through @.@ and
-- @..@. Symlinks are left unresolved, as upstream's @absPath@ leaves
-- them on a @--store@ path: a store that has always been opened through
-- a symlinked spelling has its rows keyed by that spelling, and
-- resolving it would make every one of them foreign.
module Nix.Store.Handle
( -- * Handle
Store (..),
openStore,
closeStore,
canonicalStoreDir,
-- * The collector lock
withCollectorLock,
gcLockFileName,
gcLockFilePath,
)
where
import Control.Concurrent.MVar (MVar, newMVar, putMVar, takeMVar, withMVar)
import Control.Exception (SomeException, mask, onException, throwIO, try)
import qualified Data.Text as T
import Nix.Eval.CanonPath (canonPath)
import Nix.Store.CaseSensitive (CaseSensitivity, probeCaseSensitivity)
import Nix.Store.DB (StoreDB, closeStoreDB, metaDirName, openStoreDB)
import Nix.Store.Lock (LockMode (..), PathLock, acquireLockFileWith, releasePathLock, withLockFileWith)
import Nix.Store.Path (StoreDir (..))
import System.Directory (createDirectoryIfMissing, makeAbsolute)
import System.FilePath ((</>))
-- | An open store with database and configuration. 'stDir' is the
-- canonical spelling ('canonicalStoreDir'); 'stGcLease' is the handle's
-- shared hold on the collector lock, swapped, never released early, by
-- 'withCollectorLock'.
data Store = Store
{ stDir :: !StoreDir,
stDB :: !StoreDB,
stGcLease :: !(MVar PathLock),
-- | How the volume holding the store compares sibling names, probed
-- at open ('probeCaseSensitivity') and consulted by every NAR
-- materialization into the store.
stCaseSensitivity :: !CaseSensitivity
}
-- | Upstream's name for the store-wide garbage-collector lock file.
gcLockFileName :: FilePath
gcLockFileName = "gc.lock"
-- | The collector lock file, beside the database under the metadata
-- directory.
gcLockFilePath :: StoreDir -> FilePath
gcLockFilePath dir = unStoreDir dir </> metaDirName </> gcLockFileName
-- | The spelling a store directory is keyed by: absolute, with @.@ and
-- @..@ collapsed, separators normalised and no trailing separator,
-- symlinks unresolved. That is upstream's @absPath@ of a @--store@
-- path; the module header says why symlinks stay.
canonicalStoreDir :: StoreDir -> IO StoreDir
canonicalStoreDir (StoreDir dir) = StoreDir . T.unpack . canonPath . T.pack <$> makeAbsolute dir
-- | Open a Nix store at the given directory, keyed by its canonical
-- spelling. Creates the store directory and database if they don't
-- exist. The shared lease is taken before the database is touched, so
-- an opener arriving during a collection reads nothing until the
-- collection is over. It waits with the collector's own line: upstream's
-- opener never waits here (it hands its roots to the running collector
-- through a socket instead), so there is no upstream line for the wait,
-- and the collector's names what is being waited for.
openStore :: StoreDir -> IO Store
openStore rawDir = do
dir <- canonicalStoreDir rawDir
let metaDir = unStoreDir dir </> metaDirName
lockPath = gcLockFilePath dir
createDirectoryIfMissing True metaDir
lease <- acquireLockFileWith LockShared bigGcLockMessage lockPath
db <- openStoreDB dir
-- After the directory exists: the probe answers for a path on disk,
-- and a fresh store's directory sits on the volume it will live on.
sensitivity <- probeCaseSensitivity (unStoreDir dir)
leaseVar <- newMVar lease
pure Store {stDir = dir, stDB = db, stGcLease = leaseVar, stCaseSensitivity = sensitivity}
-- | Close the store: the database first, then the lease, so the lock
-- outlives every write this handle made. Waits for an in-progress
-- 'withCollectorLock' on another thread rather than racing its swap.
closeStore :: Store -> IO ()
closeStore store = do
closeStoreDB (stDB store)
withMVar (stGcLease store) releasePathLock
-- | Upstream's announcement when the collector lock is busy.
bigGcLockMessage :: String
bigGcLockMessage = "waiting for the big garbage collector lock..."
-- | Run an action holding the collector lock exclusively. The handle's
-- shared lease is released first (the OS would otherwise count it as a
-- competing holder), the exclusive lock is taken, blocking with
-- upstream's message while other handles are open, and a fresh shared
-- lease is taken back before the handle is returned to use, on every
-- exit. Asynchronous exceptions are masked around the swap so the
-- handle is never left without a lease; the action itself runs
-- unmasked. If the renewal itself fails (the lock file cannot be
-- opened again), the surrendered lease goes back instead, so the handle
-- stays closable: 'closeStore' releases it as a no-op, and the failure
-- reaches the caller, whose handle holds no lease until it is closed.
withCollectorLock :: Store -> IO a -> IO a
withCollectorLock store act = mask $ \restore -> do
lease <- takeMVar (stGcLease store)
releasePathLock lease
outcome <- try (restore (withLockFileWith LockExclusive bigGcLockMessage lockPath (const act)))
renewed <- acquireLockFileWith LockShared bigGcLockMessage lockPath `onException` putMVar (stGcLease store) lease
putMVar (stGcLease store) renewed
either (\e -> throwIO (e :: SomeException)) pure outcome
where
lockPath = gcLockFilePath (stDir store)