Derive the migration advisory lock namespace
This commit is contained in:
parent
b196f9e17b
commit
fb94ea2c23
2 changed files with 75 additions and 8 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
module Nuthatch.Db
|
module Nuthatch.Db
|
||||||
( Db
|
( Db
|
||||||
|
, migrationAdvisoryLockKey
|
||||||
, withDb
|
, withDb
|
||||||
, migrate
|
, migrate
|
||||||
, ensurePersonalPool
|
, ensurePersonalPool
|
||||||
|
|
@ -23,9 +24,10 @@ import Data.Pool
|
||||||
, setNumStripes
|
, setNumStripes
|
||||||
, withResource
|
, withResource
|
||||||
)
|
)
|
||||||
|
import Data.Int (Int64)
|
||||||
import Database.PostgreSQL.Simple
|
import Database.PostgreSQL.Simple
|
||||||
( Connection
|
( Connection
|
||||||
, Only
|
, Only (..)
|
||||||
, close
|
, close
|
||||||
, connectPostgreSQL
|
, connectPostgreSQL
|
||||||
)
|
)
|
||||||
|
|
@ -46,6 +48,10 @@ import Nuthatch.Types
|
||||||
|
|
||||||
type Db = Pool Connection
|
type Db = Pool Connection
|
||||||
|
|
||||||
|
migrationAdvisoryLockKey :: Int64
|
||||||
|
-- Low 63 bits of the leading SHA-256("nuthatch") word keep the bigint key positive.
|
||||||
|
migrationAdvisoryLockKey = 271439036354387575
|
||||||
|
|
||||||
withDb :: PostgresConfig -> (Db -> IO a) -> IO a
|
withDb :: PostgresConfig -> (Db -> IO a) -> IO a
|
||||||
withDb cfg action = do
|
withDb cfg action = do
|
||||||
let mk = connectPostgreSQL (BS8.pack (revealSecret (cfgPgConn cfg)))
|
let mk = connectPostgreSQL (BS8.pack (revealSecret (cfgPgConn cfg)))
|
||||||
|
|
@ -57,8 +63,9 @@ withDb cfg action = do
|
||||||
migrate :: Db -> FilePath -> ProductConfig -> IO ()
|
migrate :: Db -> FilePath -> ProductConfig -> IO ()
|
||||||
migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
||||||
void
|
void
|
||||||
(PG.query_ conn
|
(PG.query conn
|
||||||
"SELECT pg_advisory_lock(564741953576419031) IS NULL"
|
"SELECT pg_advisory_lock(?) IS NULL"
|
||||||
|
(Only migrationAdvisoryLockKey)
|
||||||
:: IO [Only Bool])
|
:: IO [Only Bool])
|
||||||
result <-
|
result <-
|
||||||
runMigration conn defaultOptions
|
runMigration conn defaultOptions
|
||||||
|
|
@ -67,8 +74,9 @@ migrate db migrationDirectory cfg = withResource db $ \conn -> do
|
||||||
<> MigrationValidation (MigrationDirectory migrationDirectory)
|
<> MigrationValidation (MigrationDirectory migrationDirectory)
|
||||||
)
|
)
|
||||||
`finally` void
|
`finally` void
|
||||||
(PG.query_ conn
|
(PG.query conn
|
||||||
"SELECT pg_advisory_unlock(564741953576419031)"
|
"SELECT pg_advisory_unlock(?)"
|
||||||
|
(Only migrationAdvisoryLockKey)
|
||||||
:: IO [Only Bool])
|
:: IO [Only Bool])
|
||||||
case result of
|
case result of
|
||||||
MigrationSuccess ->
|
MigrationSuccess ->
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
module Main (main) where
|
module Main (main) where
|
||||||
|
|
||||||
import Control.Concurrent.Async (concurrently, mapConcurrently)
|
import Control.Concurrent.Async (concurrently, mapConcurrently)
|
||||||
import Control.Exception (SomeException, try)
|
import Control.Exception (SomeException, bracket_, try)
|
||||||
import Control.Monad (unless)
|
import Control.Monad (unless, void)
|
||||||
import Crypto.Hash (Digest, SHA256, hashlazy)
|
import Crypto.Hash (Digest, SHA256, hashlazy)
|
||||||
|
import Data.Bits ((.&.))
|
||||||
import qualified Data.ByteArray as BA
|
import qualified Data.ByteArray as BA
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base16 as B16
|
import qualified Data.ByteString.Base16 as B16
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import Data.Int (Int64)
|
||||||
import Data.List (sort)
|
import Data.List (sort)
|
||||||
|
import qualified Data.Pool as Pool
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text.Encoding as TE
|
import qualified Data.Text.Encoding as TE
|
||||||
import Database.PostgreSQL.Simple (Only (..))
|
import Data.Word (Word64)
|
||||||
|
import Database.PostgreSQL.Simple (Connection, Only (..))
|
||||||
|
import qualified Database.PostgreSQL.Simple as PG
|
||||||
import Nuthatch.Config
|
import Nuthatch.Config
|
||||||
( Config (..)
|
( Config (..)
|
||||||
, ProductConfig (..)
|
, ProductConfig (..)
|
||||||
|
|
@ -24,6 +30,7 @@ import Nuthatch.Db
|
||||||
, configurePoolLimit
|
, configurePoolLimit
|
||||||
, ensurePersonalPool
|
, ensurePersonalPool
|
||||||
, migrate
|
, migrate
|
||||||
|
, migrationAdvisoryLockKey
|
||||||
, withDb
|
, withDb
|
||||||
, withTransaction
|
, withTransaction
|
||||||
)
|
)
|
||||||
|
|
@ -38,6 +45,7 @@ main = do
|
||||||
withDb (cfgPostgres config) $ \database -> do
|
withDb (cfgPostgres config) $ \database -> do
|
||||||
migrate database "sql" (cfgProduct config)
|
migrate database "sql" (cfgProduct config)
|
||||||
migrate database "sql" (cfgProduct config)
|
migrate database "sql" (cfgProduct config)
|
||||||
|
verifyMigrationLock database
|
||||||
verifySchema database
|
verifySchema database
|
||||||
verifyAdmission database config
|
verifyAdmission database config
|
||||||
verifyRowDecoder database
|
verifyRowDecoder database
|
||||||
|
|
@ -45,6 +53,57 @@ main = do
|
||||||
verifyStorage config store
|
verifyStorage config store
|
||||||
putStrLn "Storage foundation integration gate passed"
|
putStrLn "Storage foundation integration gate passed"
|
||||||
|
|
||||||
|
verifyMigrationLock :: Db -> IO ()
|
||||||
|
verifyMigrationLock database = do
|
||||||
|
assertEqual
|
||||||
|
"migration lock key follows SHA-256 namespace derivation"
|
||||||
|
migrationLockKeyFromHash
|
||||||
|
migrationAdvisoryLockKey
|
||||||
|
Pool.withResource database $ \holder ->
|
||||||
|
Pool.withResource database $ \contender -> do
|
||||||
|
bracket_
|
||||||
|
(acquireAdvisoryLock holder)
|
||||||
|
(void (releaseAdvisoryLock holder))
|
||||||
|
(do
|
||||||
|
admitted <- tryAdvisoryLock contender
|
||||||
|
assert "migration lock excludes another session" (not admitted))
|
||||||
|
admitted <- tryAdvisoryLock contender
|
||||||
|
assert "migration lock admits another session after release" admitted
|
||||||
|
released <- releaseAdvisoryLock contender
|
||||||
|
assert "contending session releases migration lock" released
|
||||||
|
|
||||||
|
migrationLockKeyFromHash :: Int64
|
||||||
|
migrationLockKeyFromHash =
|
||||||
|
fromIntegral
|
||||||
|
(BS.foldl' appendByte 0 (BS.take 8 digestBytes) .&. maxSignedKey)
|
||||||
|
where
|
||||||
|
digestBytes = BA.convert (hashlazy "nuthatch" :: Digest SHA256)
|
||||||
|
appendByte accumulator byte =
|
||||||
|
accumulator * 256 + fromIntegral byte
|
||||||
|
maxSignedKey = 0x7fffffffffffffff :: Word64
|
||||||
|
|
||||||
|
acquireAdvisoryLock :: Connection -> IO ()
|
||||||
|
acquireAdvisoryLock connection =
|
||||||
|
void
|
||||||
|
(PG.query connection
|
||||||
|
"SELECT pg_advisory_lock(?) IS NULL"
|
||||||
|
(Only migrationAdvisoryLockKey)
|
||||||
|
:: IO [Only Bool])
|
||||||
|
|
||||||
|
tryAdvisoryLock :: Connection -> IO Bool
|
||||||
|
tryAdvisoryLock connection = do
|
||||||
|
[Only admitted] <- PG.query connection
|
||||||
|
"SELECT pg_try_advisory_lock(?)"
|
||||||
|
(Only migrationAdvisoryLockKey)
|
||||||
|
pure admitted
|
||||||
|
|
||||||
|
releaseAdvisoryLock :: Connection -> IO Bool
|
||||||
|
releaseAdvisoryLock connection = do
|
||||||
|
[Only released] <- PG.query connection
|
||||||
|
"SELECT pg_advisory_unlock(?)"
|
||||||
|
(Only migrationAdvisoryLockKey)
|
||||||
|
pure released
|
||||||
|
|
||||||
verifySchema :: Db -> IO ()
|
verifySchema :: Db -> IO ()
|
||||||
verifySchema database = do
|
verifySchema database = do
|
||||||
tables <- Db.query_ database
|
tables <- Db.query_ database
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue