-
Notifications
You must be signed in to change notification settings - Fork 844
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Take a write lock before using Pantry database #4471
This makes two basic changes to the code: * Avoids a connection pool, which forces the SQLite connection to be closed at the end of each call to withStorage. This seems unfortunate; we would like to be able to maintain connections. However, SQLite seems to view such connections as holding a write lock even when not in the middle of a database transaction. * Uses an explicit file lock on the two longest-running write operations: migration and Hackage population. Ideally the locking mechanism in SQLite would be sufficient for this, but (1) Hackage population can take a long time, and (2) evidence has shown that it doesn't behave as we expect it to. This may be insufficient for preventing the bug described in #4471, in which case we may need to apply withWriteLock to more calls. However, I'd like to start off small to avoid the additional overhead.
- Loading branch information
Showing
6 changed files
with
91 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,47 +1,93 @@ | ||
{-# LANGUAGE NoImplicitPrelude #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
module Pantry.SQLite | ||
( P.Storage | ||
( Storage (..) | ||
, initStorage | ||
, withStorage | ||
) where | ||
|
||
import RIO hiding (FilePath) | ||
import qualified Pantry.Types as P | ||
import Database.Persist.Sqlite | ||
import RIO.Orphans () | ||
import Path (Path, Abs, File, toFilePath, parent) | ||
import Path.IO (ensureDir) | ||
import Data.Pool (destroyAllResources) | ||
import Pantry.Types (PantryException (MigrationFailure)) | ||
import Pantry.Types (PantryException (MigrationFailure), Storage (..)) | ||
import System.FileLock (withFileLock, withTryFileLock, SharedExclusive (..)) | ||
|
||
initStorage | ||
:: HasLogFunc env | ||
=> Text | ||
-> Migration | ||
-> Path Abs File -- ^ storage file | ||
-> (P.Storage -> RIO env a) | ||
-> (Storage -> RIO env a) | ||
-> RIO env a | ||
initStorage description migration fp inner = do | ||
ensureDir $ parent fp | ||
bracket | ||
(createSqlitePoolFromInfo (sqinfo False) 1) | ||
(liftIO . destroyAllResources) $ \pool -> do | ||
migrates <- wrapMigrationFailure $ runSqlPool (runMigrationSilent migration) pool | ||
forM_ migrates $ \mig -> logDebug $ "Migration executed: " <> display mig | ||
bracket | ||
(createSqlitePoolFromInfo (sqinfo True) 1) | ||
(liftIO . destroyAllResources) $ \pool -> inner (P.Storage pool) | ||
|
||
migrates <- withWriteLock fp $ wrapMigrationFailure $ | ||
withSqliteConnInfo (sqinfo True) $ runReaderT $ | ||
runMigrationSilent migration | ||
forM_ migrates $ \mig -> logDebug $ "Migration executed: " <> display mig | ||
|
||
inner $ Storage | ||
{ withStorage_ = withSqliteConnInfo (sqinfo False) . runSqlConn | ||
, withWriteLock_ = withWriteLock fp | ||
} | ||
where | ||
wrapMigrationFailure = handleAny (throwIO . MigrationFailure description fp) | ||
sqinfo fk = set extraPragmas ["PRAGMA busy_timeout=2000;"] | ||
$ set fkEnabled fk | ||
|
||
sqinfo isMigration | ||
= set extraPragmas ["PRAGMA busy_timeout=2000;"] | ||
|
||
-- When doing a migration, we want to disable foreign key | ||
-- checking, since the order in which tables are created by | ||
-- the migration scripts may not respect foreign keys. The | ||
-- rest of the time: enforce those foreign keys. | ||
$ set fkEnabled (not isMigration) | ||
|
||
-- This one is subtle. Enabling Write-Ahead Logging (WAL) | ||
-- mode is persistent: the next usage of the database is | ||
-- still in WAL mode until explicitly disabled. Therefore, | ||
-- when we're already migrated the database, there's no | ||
-- need to waste time reenabling WAL. Skipping this also | ||
-- allows us to get slightly more meaningful error messages | ||
-- if we run into the SQLITE_BUSY bug again. | ||
$ set walEnabled isMigration | ||
|
||
$ mkSqliteConnectionInfo (fromString $ toFilePath fp) | ||
|
||
withStorage | ||
-- | Ensure that only one process is trying to write to the database | ||
-- at a time. See | ||
-- https://github.com/commercialhaskell/stack/issues/4471 and comments | ||
-- above. | ||
withWriteLock | ||
:: HasLogFunc env | ||
=> ReaderT SqlBackend (RIO env) a | ||
-> P.Storage | ||
=> Path Abs File -- ^ SQLite database file | ||
-> RIO env a | ||
-> RIO env a | ||
withStorage action (P.Storage pool) = | ||
runSqlPool action pool | ||
withWriteLock dbFile inner = do | ||
let lockFile = toFilePath dbFile ++ ".pantry-write-lock" | ||
withRunInIO $ \run -> do | ||
mres <- withTryFileLock lockFile Exclusive $ const $ run inner | ||
case mres of | ||
Just res -> pure res | ||
Nothing -> do | ||
run $ logInfo "Unable to get a write lock on the Pantry database, waiting..." | ||
shouldStopComplainingVar <- newTVarIO False | ||
let complainer = fix $ \loop -> do | ||
delay <- registerDelay $ 60 * 1000 * 1000 -- 1 minute | ||
shouldComplain <- | ||
atomically $ | ||
-- Delay has triggered, time to complain again | ||
(readTVar delay >>= checkSTM >> pure True) <|> | ||
-- Time to stop complaining, ignore that delay immediately | ||
(readTVar shouldStopComplainingVar >>= checkSTM >> pure False) | ||
when shouldComplain $ do | ||
run $ logWarn "Still waiting on the Pantry database write lock..." | ||
loop | ||
stopComplaining = atomically $ writeTVar shouldStopComplainingVar True | ||
worker = withFileLock lockFile Exclusive $ const $ do | ||
run $ logInfo "Acquired the Pantry database write lock" | ||
stopComplaining | ||
run inner | ||
runConcurrently $ Concurrently complainer | ||
*> Concurrently (worker `finally` stopComplaining) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters