packages feed

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 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