packages feed

hasql-pool 1.5.1.0 → 1.5.1.1

raw patch · 5 files changed

+97/−11 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,3 +1,19 @@+# v1.5.1.1++## Fixes++- After the server closes an idle pooled connection (a Postgres restart,+  `pg_terminate_backend`, a server-side idle timeout), the first session on+  it can fail with `UnexpectedRowCountStatementError` instead of a connection+  error. Hasql maps the extra result that reports the closed connection to+  that error, and a `singleRow` statement that returns two rows fails the+  same way on a healthy connection, so the error alone cannot justify a+  discard: the dead connection got returned to the pool and failed one more+  session before it was discarded, so after a database restart every pooled+  connection failed two sessions. Now, when a session fails with an error+  that does not require a discard, `use` also checks the status that the+  adapter reports for the connection and discards it when it is marked bad.+ # v1.5.1.0  ## Fixes
hasql-pool.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hasql-pool-version: 1.5.1.0+version: 1.5.1.1 category: Hasql, Database, PostgreSQL synopsis: Pool of connections for Hasql homepage: https://github.com/nikita-volkov/hasql-pool
src/integration-tests/Specs/UseSpec.hs view
@@ -39,6 +39,27 @@         _ -> False       use pool (Sessions.getSetting settingName) `shouldReturn` Right Nothing +  -- After a server restart or @pg_terminate_backend@ the first statement on+  -- the closed connection can fail with an 'Errors.UnexpectedRowCountStatementError'+  -- instead of a 'Errors.ConnectionSessionError'. The pool must not hand the+  -- connection out again.+  it "Connections closed by the server cause eviction of connection" \scopeParams ->+    Scripts.onAutotaggedPool 1 10 1_800 1_800 scopeParams \appName pool -> do+      use pool Sessions.selectOne `shouldReturn` Right 1+      Scripts.onAutotaggedPool 1 10 1_800 1_800 scopeParams \_ adminPool ->+        use adminPool (terminateBackends appName) `shouldReturn` Right [True]+      _ <- use pool Sessions.selectOne+      use pool Sessions.selectOne `shouldReturn` Right 1++  it "Unexpected row counts on a healthy connection don't cause eviction of connection" \scopeParams ->+    Scripts.onAutotaggedPool 1 10 1_800 1_800 scopeParams \_ pool -> do+      Right pid <- use pool backendPid+      result <- use pool selectTwoRowsAsSingleRow+      result `shouldSatisfy` \case+        Left (SessionUsageError (Errors.StatementSessionError _ _ _ _ _ Errors.UnexpectedRowCountStatementError {})) -> True+        _ -> False+      use pool backendPid `shouldReturn` Right pid+   it "Connection gets returned to the pool after normal use" \scopeParams ->     Scripts.onAutotaggedPool 1 10 1_800 1_800 scopeParams \_ pool -> do       _ <- use pool $ Sessions.selectOne@@ -173,6 +194,36 @@         ("select $1 :: " <> quoteIdentifier typeName)         (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing typeName id)))         (Decoders.singleRow (Decoders.column (Decoders.nonNullable (Decoders.enum Nothing typeName Just))))++terminateBackends :: Text -> Session.Session [Bool]+terminateBackends appName =+  Session.statement appName statement+  where+    statement =+      Statement.unpreparable+        "select pg_terminate_backend(pid, 10000) from pg_stat_activity where application_name = $1"+        (Encoders.param (Encoders.nonNullable Encoders.text))+        (Decoders.rowList (Decoders.column (Decoders.nonNullable Decoders.bool)))++backendPid :: Session.Session Int32+backendPid =+  Session.statement () statement+  where+    statement =+      Statement.preparable+        "select pg_backend_pid()"+        Encoders.noParams+        (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4)))++selectTwoRowsAsSingleRow :: Session.Session Int64+selectTwoRowsAsSingleRow =+  Session.statement () statement+  where+    statement =+      Statement.preparable+        "select * from (values (1 :: int8), (2 :: int8)) as t"+        Encoders.noParams+        (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int8)))  driverError :: Session.Session () driverError =
src/library/Hasql/Pool.hs view
@@ -193,7 +193,8 @@ -- | Use a connection from the pool to run a session and return the connection -- to the pool, when finished. ----- Session failing with a 'Session.ClientError' gets interpreted as a loss of+-- Session failing with a 'Errors.ConnectionSessionError' or leaving the+-- connection in the 'Pqi.ConnectionBad' state gets interpreted as a loss of -- connection. In such case the connection does not get returned to the pool -- and a slot gets freed up for a new connection to be established the next -- time one is needed. The error still gets returned from this function.@@ -346,8 +347,12 @@         Left exc -> do           returnConn reuseVar entry           throwIO exc-        Right (Left err) ->-          if ErrorsDestruction.requiresConnectionDiscard err+        Right (Left err) -> do+          discard <-+            if ErrorsDestruction.requiresConnectionDiscard err+              then return True+              else connectionIsLost (entryConnection entry)+          if discard             then do               Connection.release (entryConnection entry)               atomically returnSlot@@ -355,7 +360,7 @@                 $ poolObserver                   ( ConnectionObservation                       (entryId entry)-                      (TerminatedConnectionStatus (NetworkErrorConnectionTerminationReason (ErrorsDestruction.discardDetails err)))+                      (TerminatedConnectionStatus (NetworkErrorConnectionTerminationReason (Just (Errors.toMessage err))))                   )               return $ Left $ SessionUsageError err             else do@@ -387,6 +392,26 @@             Connection.release (entryConnection entry)             atomically returnSlot             poolObserver (ConnectionObservation (entryId entry) (TerminatedConnectionStatus ReleaseConnectionTerminationReason))++-- | Whether the adapter has marked the connection as lost.+--+-- When the server closes an idle connection, e.g. on restart or with+-- @pg_terminate_backend@, the next statement on it can fail with an error+-- that doesn't say so: 'Errors.UnexpectedRowCountStatementError', for the+-- extra result that reports the closed connection. A statement returning more+-- rows than its decoder expects fails the same way on a healthy connection, so+-- the error can't tell the two apart. The connection status can: the adapter+-- has set it to 'Pqi.ConnectionBad' by then.+connectionIsLost :: Connection -> IO Bool+connectionIsLost connection =+  Connection.use connection checkStatus <&> \case+    Left _ -> True+    Right lost -> lost+  where+    checkStatus =+      Session.onLibpqConnection \pqConnection -> do+        status <- Pqi.status pqConnection+        return (Right (status == Pqi.ConnectionBad), pqConnection)  -- | Whether a pool request is waiting. The ones that stopped waiting get -- dropped.
src/library/Hasql/Pool/SessionErrorDestructors.hs view
@@ -19,12 +19,6 @@   -- cleanup after an interruption fails, so it must not be reused by the pool.   Errors.DriverSessionError {} -> True -discardDetails :: Errors.SessionError -> Maybe Text-discardDetails err =-  if requiresConnectionDiscard err-    then Just $ Errors.toMessage err-    else Nothing- statementRequiresConnectionDiscard :: Errors.StatementError -> Bool statementRequiresConnectionDiscard = \case   Errors.ServerStatementError serverError -> isStaleServerError serverError