packages feed

sydtest-persistent-postgresql 0.5.1.0 → 0.5.2.0

raw patch · 4 files changed

+80/−2 lines, 4 files

Files

CHANGELOG.md view
@@ -1,5 +1,26 @@ # Changelog +## [0.5.2.0] - 2026-09-26++### Fixed++* A read against the standby is no longer destroyed by a vacuum on the+  primary. The standby was configured to serve reads and to stay deliberately+  behind, and then left to postgres' defaults on what should happen when+  recovery met one of those reads: the primary was never told what a standby+  reader was holding, so it was free to remove those row versions, and+  replaying the cleanup cancelled the reader. The read came back as `SQLSTATE+  40001`, or, when the session was holding a snapshot, as a dropped+  connection.++  The standby now sends `hot_standby_feedback`, so the primary keeps what the+  standby's readers still need, and reports it often enough to matter for+  databases that live as briefly as these do. Recovery also waits for a+  conflicting read rather than cancelling it, which covers the conflicts+  feedback does not: a cleanup already in flight, or a lock that replayed DDL+  wants. A replica in a test suite is there to be behind, and a test that+  wants it caught up says so with `awaitReplica`.+ ## [0.5.1.0] - 2026-09-14  ### Changed
src/Test/Syd/Persistent/Postgresql.hs view
@@ -270,7 +270,27 @@           [ [ "port = " ++ show primaryPort,               "unix_socket_directories = '" ++ socketDir ++ "'",               "listen_addresses = ''",-              "hot_standby = on"+              "hot_standby = on",+              -- Tell the primary what this standby's readers still need, so+              -- it keeps those row versions instead of vacuuming them away+              -- and leaving recovery to cancel the reader that wanted them.+              -- The base backup took a replication slot, which is what+              -- carries the feedback, so this has somewhere to be recorded.+              "hot_standby_feedback = on",+              -- Feedback is only sent this often, and the default of ten+              -- seconds is longer than the databases here live, so without+              -- this the primary would learn what to keep long after it had+              -- already thrown it away.+              "wal_receiver_status_interval = 1s",+              -- Feedback closes the window rather than the door: it cannot+              -- retract a cleanup already in flight, and it says nothing+              -- about the conflicts that are not about row versions at all,+              -- such as a lock a replayed DDL wants. Recovery waiting is the+              -- right answer to those here, because a replica in a test+              -- suite is there to be behind, and a test that wanted it+              -- caught up says so with 'awaitReplica'.+              "max_standby_streaming_delay = -1",+              "max_standby_archive_delay = -1"             ],             ["recovery_min_apply_delay = '" ++ renderReplicaLag replicaConfigLag ++ "'"]           ]
sydtest-persistent-postgresql.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           sydtest-persistent-postgresql-version:        0.5.1.0+version:        0.5.2.0 synopsis:       An persistent-postgresql companion library for sydtest category:       Testing homepage:       https://github.com/NorfairKing/sydtest#readme
test/Test/Syd/Persistent/Postgresql/StandbySpec.hs view
@@ -4,6 +4,7 @@ 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@@ -42,3 +43,39 @@         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