pqi-native 1.0.1.9 → 1.0.1.10
raw patch · 5 files changed
+70/−24 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- pqi-native.cabal +1/−1
- src/library/Pqi/Native.hs +18/−6
- src/library/Pqi/Native/Connection.hs +12/−0
- src/library/Pqi/Native/Query.hs +33/−17
CHANGELOG.md view
@@ -1,3 +1,9 @@+# v1.0.1.10++## Fixes++- Fixed a socket death during a send (e.g. `EPIPE`/`ECONNRESET`) escaping `Pqi.sendQuery`/`sendQueryParams`/`sendPrepare`/`sendQueryPrepared`/`sendDescribePrepared`/`sendDescribePortal` as a raw, uncaught `IOException` instead of the `False` libpq's `PQsendQuery` and friends always return for a fatal send (marking `PQstatus` `CONNECTION_BAD` and leaving `PQerrorMessage` with the same "server closed the connection unexpectedly" wording used for a connection lost while reading). `Pqi.Native.Transport.send` is `Network.Socket.ByteString.sendAll`, which throws rather than reporting failure through a return value, and `sendAsync` - the function every one of those six calls funnels through - called it with no exception handler at all. This is also the entry point `hasql`'s `Session` machinery actually exercises for every ordinary (non-pipelined) statement, so the escaped exception used to reach `Hasql.Connection.use`'s interruption handling and get treated as an async interruption instead of a classified send failure. `sendAsync` now catches an escaped `IOException`, marks the connection bad via the same `markConnectionLost` classification the read side already had, and returns `False`. The sibling direct sends in `pipelineSync`/`sendFlushRequest` (which bypass `sendAsync` entirely) get the same treatment. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.SendQuery.ConnectionLostBeforeSend`.+ # v1.0.1.9 ## Fixes
pqi-native.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: pqi-native-version: 1.0.1.9+version: 1.0.1.10 category: Database, PostgreSQL synopsis: Native (pure-Haskell) adapter for pqi description:
src/library/Pqi/Native.hs view
@@ -9,6 +9,7 @@ ) where +import Control.Exception (IOException, catch) import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as ByteString.Char8 import qualified Data.Map.Strict as Map@@ -144,12 +145,23 @@ if pending then pure False else writeIORef (Connection.pipelineStatus connection) Pqi.PipelineOff $> True,- Pqi.pipelineSync = do- Connection.sendMessage connection syncMessage- modifyIORef' (Connection.pendingSyncs connection) (+ 1)- writeIORef (Connection.asyncPending connection) True- pure True,- Pqi.sendFlushRequest = Connection.sendMessage connection flushMessage $> True,+ -- Both of these send directly, bypassing 'Query.sendAsync' (there is no+ -- result-bearing command to track), so they need their own escape+ -- hatch for the same class of failure 'Query.sendAsync' guards+ -- against: a socket death mid-send throwing instead of the @False@+ -- @PQpipelineSync@\/@PQsendFlushRequest@ return on a fatal send.+ Pqi.pipelineSync =+ catch+ do+ Connection.sendMessage connection syncMessage+ modifyIORef' (Connection.pendingSyncs connection) (+ 1)+ writeIORef (Connection.asyncPending connection) True+ pure True+ (\err -> False <$ Connection.markConnectionLost connection err),+ Pqi.sendFlushRequest =+ catch+ (Connection.sendMessage connection flushMessage $> True)+ (\err -> False <$ Connection.markConnectionLost connection err), Pqi.getCancel = do key <- readIORef (Connection.backendKey connection) pure
src/library/Pqi/Native/Connection.hs view
@@ -15,6 +15,7 @@ fieldValue, setError, connectionLostMessage,+ markConnectionLost, ) where @@ -420,6 +421,17 @@ | isConnectionLost err = "server closed the connection unexpectedly\n\tThis probably means the server terminated abnormally\n\tbefore or while processing the request." | otherwise = ByteString.Char8.pack (show err)++-- | Record an escaped 'IOException' from the transport - a send or a read,+-- at any point in a flow - the way libpq's own internals do: never throw,+-- just mark the connection bad with the classified message ('connectionLostMessage'),+-- so every subsequent call - starting with the one already in flight - sees+-- it via 'Pqi.status'\/'Pqi.errorMessage'.+markConnectionLost :: Connection -> IOException -> IO ByteString+markConnectionLost connection err = do+ let message = connectionLostMessage err+ setError connection (message <> "\n")+ pure message -- | The handshake-failure message ('failWith', inside 'handshake') for a -- Unix-domain socket connection: names the socket path rather than a
src/library/Pqi/Native/Query.hs view
@@ -77,47 +77,64 @@ -- | Simple query. Returns the last result, mirroring @PQexec@. exec :: Connection -> ByteString -> IO (Maybe NativeResult) exec connection sql = withReady connection do- sendMessage connection (queryMessage sql)- catch (lastMaybe <$> collectSimple connection sql) (fmap Just . connectionLostResult connection sql)+ catch+ (sendMessage connection (queryMessage sql) >> (lastMaybe <$> collectSimple connection sql))+ (fmap Just . connectionLostResult connection sql) -- | Parameterized query via the extended protocol. execParams :: Connection -> ByteString -> [Maybe (Word32, ByteString, Format)] -> Format -> IO (Maybe NativeResult) execParams connection sql params resultFormat | tooManyParams params = pure Nothing | otherwise = withReady connection do- sendMessage connection (paramsWrite sql params resultFormat)- catch (Just <$> collectExtended connection sql) (fmap Just . connectionLostResult connection sql)+ catch+ (sendMessage connection (paramsWrite sql params resultFormat) >> (Just <$> collectExtended connection sql))+ (fmap Just . connectionLostResult connection sql) -- | Prepare a named statement. prepare :: Connection -> ByteString -> ByteString -> Maybe [Word32] -> IO (Maybe NativeResult) prepare connection name sql parameterTypes | maybe False tooManyParams parameterTypes = pure Nothing | otherwise = withReady connection do- sendMessage connection (prepareWrite name sql parameterTypes)- catch (Just <$> collectExtended connection sql) (fmap Just . connectionLostResult connection sql)+ catch+ (sendMessage connection (prepareWrite name sql parameterTypes) >> (Just <$> collectExtended connection sql))+ (fmap Just . connectionLostResult connection sql) -- | Execute a previously prepared statement. execPrepared :: Connection -> ByteString -> [Maybe (ByteString, Format)] -> Format -> IO (Maybe NativeResult) execPrepared connection name params resultFormat | tooManyParams params = pure Nothing | otherwise = withReady connection do- sendMessage connection (preparedWrite name params resultFormat)- catch (Just <$> collectExtended connection "") (fmap Just . connectionLostResult connection "")+ catch+ (sendMessage connection (preparedWrite name params resultFormat) >> (Just <$> collectExtended connection ""))+ (fmap Just . connectionLostResult connection "") -- * Asynchronous flows -- | Send a write in async mode, tracking pending commands for pipeline abort.+--+-- A socket death mid-send (e.g. @EPIPE@\/@ECONNRESET@) surfaces here as an+-- escaped 'IOException' from 'Transport.send', which 'sendMessage' does not+-- catch. Left uncaught, it would blow straight through this function - and+-- every caller layered on it ('Pqi.Native.sendQuery' etc., 'Hasql.Comms.Send')+-- - as a raw exception instead of the @False@ that @PQsendQuery@ always+-- returns for a fatal send. Catching it here and marking the connection bad+-- keeps the contract: the caller sees a normal failed send, discoverable via+-- 'Pqi.status', exactly as libpq's own internals never throw and always+-- record the failure on the connection instead. sendAsync :: Connection -> ByteString -> Poker.Write -> IO Bool sendAsync connection sql write = do status <- readIORef (connStatus connection) case status of- ConnectionOk -> do- sendMessage connection write- writeIORef (currentQuery connection) sql- writeIORef (asyncPending connection) True- pipeStatus <- readIORef (pipelineStatus connection)- when (pipeStatus /= PipelineOff) $ modifyIORef' (pendingCommands connection) (+ 1)- pure True+ ConnectionOk ->+ catch+ do+ sendMessage connection write+ writeIORef (currentQuery connection) sql+ writeIORef (asyncPending connection) True+ pipeStatus <- readIORef (pipelineStatus connection)+ when (pipeStatus /= PipelineOff) $ modifyIORef' (pendingCommands connection) (+ 1)+ pure True+ (\err -> False <$ markConnectionLost connection err) _ -> pure False -- | Whether the connection is in pipeline mode.@@ -432,8 +449,7 @@ -- connection bad so the caller's next call sees it too. connectionLostResult :: Connection -> ByteString -> IOException -> IO NativeResult connectionLostResult connection sql err = do- let message = connectionLostMessage err- setError connection (message <> "\n")+ message <- markConnectionLost connection err pure (NativeResult FatalError [] [] Nothing (Map.singleton 0x4d message) [] sql) -- accumulator for a result under construction