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 +16/−0
- hasql-pool.cabal +1/−1
- src/integration-tests/Specs/UseSpec.hs +51/−0
- src/library/Hasql/Pool.hs +29/−4
- src/library/Hasql/Pool/SessionErrorDestructors.hs +0/−6
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