sydtest-persistent-postgresql-0.5.2.0: test/Test/Syd/Persistent/Postgresql/StandbySpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Test.Syd.Persistent.Postgresql.StandbySpec (spec) where
import Database.Persist
import Database.Persist.Sql (rawExecute, runSqlPoolNoTransaction)
import Database.PostgreSQL.Simple (SqlError (..))
import Test.Syd
import Test.Syd.Persistent.Example
import Test.Syd.Persistent.Postgresql
spec :: Spec
spec =
describe "persistPostgresqlReplicatedSpec" $
persistPostgresqlReplicatedSpec migrateExample $ do
it "does not show a write on the replica while it is behind" $ \pools -> do
let p = Person {personName = "John Doe", personAge = Just 21}
i <- onPrimary pools $ insert p
mPerson <- onReplica pools $ get i
mPerson `shouldBe` Nothing
it "shows the write on the replica once it has caught up" $ \pools -> do
let p = Person {personName = "Jane Doe", personAge = Just 22}
i <- onPrimary pools $ insert p
awaitReplica pools
mPerson <- onReplica pools $ get i
mPerson `shouldBe` Just p
-- A hot standby is read-only because postgres refuses the write, not
-- because the test was told to pretend.
it "refuses a write through the replica" $ \pools -> do
let p = Person {personName = "Jim Doe", personAge = Just 23}
onReplica pools (insert_ p)
`shouldThrow` (\(e :: SqlError) -> sqlState e == "25006")
-- An application configured without a read replica reads and writes
-- through the one pool it has, and everything here still works on it.
it "reads back at once through pools that are not replicated" $ \pools -> do
let unreplicated = unreplicatedPools (replicatedPoolsPrimary pools)
let p = Person {personName = "Jess Doe", personAge = Just 25}
i <- onPrimary unreplicated $ insert p
awaitReplica unreplicated
mPerson <- onReplica unreplicated $ get i
mPerson `shouldBe` Just p
-- A read on the standby holds a snapshot. If the primary is free to
-- remove the row versions that snapshot needs, replaying the cleanup
-- leaves the standby no choice but to cancel the read, and the
-- application sees SQLSTATE 40001 rather than its data.
--
-- Nothing the application does can avoid that, so the standby has to
-- tell the primary what it is holding.
it "keeps a read on the replica alive while the primary vacuums what it is reading" $ \pools -> do
let people = [Person {personName = "Vacuumed " ++ show i, personAge = Just i} | i <- [1 :: Int .. 100]]
_ <- onPrimary pools $ insertMany people
awaitReplica pools
onReplica pools $ do
-- Read committed takes a new snapshot per statement and lets the
-- old one go, so there would be nothing held across the cleanup.
-- An application that reads twice and compares needs this.
rawExecute "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ" []
-- Take the snapshot the cleanup below has to be kept away from.
firstRead <- selectList ([] :: [Filter Person]) []
liftIO $ length firstRead `shouldBe` 100
liftIO $ do
-- Every row gets a new version, so every old version is garbage,
-- and the vacuum writes the cleanup that conflicts.
onPrimary pools $ updateWhere ([] :: [Filter Person]) [PersonAge =. Just 0]
-- VACUUM refuses to run inside a transaction block.
runSqlPoolNoTransaction
(rawExecute "VACUUM person" [])
(replicatedPoolsPrimary pools)
Nothing
awaitReplica pools
secondRead <- selectList ([] :: [Filter Person]) []
liftIO $ length secondRead `shouldBe` 100