packages feed

pqi-native 1.0.1.8 → 1.0.1.9

raw patch · 5 files changed

+76/−20 lines, 5 filesdep ~pqi-conformancePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: pqi-conformance

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,8 +1,18 @@+# v1.0.1.9++## Fixes++- Fixed a DNS resolution failure during `connectdb` reporting the raw `Show`n `Network.Socket.getAddrInfo` exception (e.g. `could not connect to server: Network.Socket.getAddrInfo (called with preferred socket type/protocol: ...): does not exist (nodename nor servname provided, or not known)`) instead of libpq's own sentence for it (`could not translate host name "..." to address: nodename nor servname provided, or not known`). `connectFailureMessage` now recognizes a resolver failure by `Network.Socket.getAddrInfo` naming itself in the exception's `ioe_location`, and reproduces libpq's wording using the exception's `ioe_description`. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.Connectdb.UnresolvableHost`.++- Fixed a handshake-time hard TCP reset (`ECONNRESET`, as opposed to a clean EOF) reporting the raw `Show`n `IOException` (e.g. `Network.Socket.recvBuf: resource vanished (Connection reset by peer)`) instead of libpq's own "server closed the connection unexpectedly" sentence, which it uses for both a reset and a clean EOF alike. `handshakeFailureMessage`'s classification (`isConnectionLost`, factored out as `connectionLostMessage`) now also recognizes a reset - `Network.Socket` surfaces `ECONNRESET` as `ioe_type == ResourceVanished` rather than `System.IO.Error.eofErrorType` - not just a clean EOF. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.Connectdb.HandshakeReset`.++- Fixed a connection reset while a query response was in flight escaping `Pqi.exec`/`execParams`/`prepare`/`execPrepared`/`describePrepared`/`describePortal` as a raw, uncaught `IOException` instead of coming back as a classified `FatalError` result the way libpq's `PQexec` does (a lost connection mid-query never throws there; it reports a result with `FatalError` status and the same "server closed the connection unexpectedly" wording used for a handshake-time loss). Each of those six flows now catches an `IOException` escaping its read loop and reports it through the same `connectionLostMessage` classification, also marking the connection `ConnectionBad`. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.Exec.ConnectionLostMidQuery`. Found via `hasql` issue #329.+ # v1.0.1.8  ## Fixes -- Fixed `connectFailureMessage` (used when the initial `connect(2)` fails, e.g. a missing Unix-socket directory) hand-rolling a TCP-shaped message - the raw `Show`n `IOException` prefixed with `"could not connect to server: "` and a `(Unix domain socket '<path>')` parenthetical appended - instead of reporting the failure the way libpq does. That hand-rolled text retained the literal prefix `"could not connect to server: "`, which is also the substring `Hasql.Connection`'s error classifier matches to identify a transient networking failure - so a permanent misconfiguration (missing socket directory) was misclassified as transient. The Unix-socket branch now delegates to `unixSocketFailureMessage`, extracting the underlying `IOException`'s `ioe_description` (the raw OS `strerror` text, e.g. `"No such file or directory"`) instead of embedding its whole `Show`n form, and appending the same `"Is the server running locally and accepting connections on that socket?"` hint libpq appends for this failure - matching libpq's message for this case byte-for-byte. Also fixed `unixSocketFailureMessage` itself quoting the socket path with single quotes (`'<path>'`) instead of libpq's double quotes (`"<path>"`), which affects every message it formats, including the handshake-rejection path this Unix-socket branch now shares. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.Connectdb.MissingUnixSocketDirectory`. Found via `hasql` issue #329.+- Fixed `connectFailureMessage` (used when the initial `connect(2)` fails, e.g. a missing Unix-socket directory) hand-rolling a TCP-shaped message - the raw `Show`n `IOException` prefixed with `"could not connect to server: "` and a `(Unix domain socket '<path>')` parenthetical appended - instead of reporting the failure the way libpq does. That hand-rolled text retained the literal prefix `"could not connect to server: "`, which is also the substring `Hasql.Connection`'s error classifier matches to identify a transient networking failure - so a permanent misconfiguration (missing socket directory) was misclassified as transient. The Unix-socket branch now delegates to `unixSocketFailureMessage`, extracting the underlying `IOException`'s `ioe_description` (the raw OS `strerror` text, e.g. `"No such file or directory"`) instead of embedding its whole `Show`n form, and appending the same `"Is the server running locally and accepting connections on that socket?"` hint libpq appends for this failure - matching libpq's message for this case exactly. Also fixed `unixSocketFailureMessage` itself quoting the socket path with single quotes (`'<path>'`) instead of libpq's double quotes (`"<path>"`), which affects every message it formats, including the handshake-rejection path this Unix-socket branch now shares. Caught by the `pqi-conformance` spec `Pqi.Conformance.Operation.Connectdb.MissingUnixSocketDirectory`. Found via `hasql` issue #329.  # v1.0.1.7 
pqi-native.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: pqi-native-version: 1.0.1.8+version: 1.0.1.9 category: Database, PostgreSQL synopsis: Native (pure-Haskell) adapter for pqi description:@@ -11,7 +11,7 @@   @ptr-peeker@; the public library is an abstraction over it that builds the   @pqi@ @Adapter@. -  __Status: Alpha.__ Verified byte-for-byte against @postgresql-libpq@ via a+  __Status: Alpha.__ Verified exactly against @postgresql-libpq@ via a   differential conformance suite, but not yet proven in production. It   implements the same @pqi@ interface as   [@pqi-ffi@](https://hackage.haskell.org/package/pqi-ffi), the stable@@ -158,5 +158,5 @@   build-depends:     base >=4.11 && <5,     hspec >=2.11 && <2.12,-    pqi-conformance ^>=1.0.8,+    pqi-conformance ^>=1.0.9,     pqi-native,
src/library/Pqi/Native/Connection.hs view
@@ -14,16 +14,18 @@     sendMessage,     fieldValue,     setError,+    connectionLostMessage,   ) where  import Control.Exception (IOException, SomeException, catch, try) import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as ByteString.Char8+import Data.List (isInfixOf) import qualified Data.Map.Strict as Map import qualified Data.Sequence as Seq import qualified Data.Set as Set-import GHC.IO.Exception (ioe_description)+import GHC.IO.Exception (IOErrorType (ResourceVanished), ioe_description, ioe_location, ioe_type) import Pqi (ConnStatus (..), Notify (..), PipelineStatus (..), Verbosity (..)) import qualified Pqi.Native.Auth as Auth import Pqi.Native.Prelude@@ -357,7 +359,8 @@   writeIORef (connStatus connection) ConnectionBad  -- | Format an initial-connect failure (the socket couldn't even be opened),--- matching libpq's distinct phrasing for a Unix-domain socket vs. a TCP host.+-- matching libpq's distinct phrasing for a Unix-domain socket, a DNS+-- resolution failure, and any other TCP connect failure. connectFailureMessage :: ConnInfo -> IOException -> ByteString connectFailureMessage connInfo err   | Transport.isUnixSocketHost (host connInfo) =@@ -366,6 +369,17 @@         ( ByteString.Char8.pack (ioe_description err)             <> "\n\tIs the server running locally and accepting connections on that socket?\n"         )+  -- 'Network.Socket.getAddrInfo' names itself in 'ioe_location' on failure+  -- (there is no other reliable way to tell a resolver failure apart from a+  -- 'Network.Socket.connect' failure once both have collapsed to a plain+  -- 'IOException'), and libpq has its own distinct sentence for this case+  -- rather than the generic "could not connect to server".+  | "getAddrInfo" `isInfixOf` ioe_location err =+      "could not translate host name \""+        <> host connInfo+        <> "\" to address: "+        <> ByteString.Char8.pack (ioe_description err)+        <> "\n"   | otherwise = "could not connect to server: " <> ByteString.Char8.pack (show err)  -- | Format a handshake-time 'IOException' - e.g. the server closing the@@ -375,17 +389,37 @@ -- 'tcpFailureMessage' wrapper 'failWith' uses for a rejected 'ErrorResponse', -- so a failure that interrupts the handshake reads exactly like any other -- classified rejection instead of escaping 'establish' as an uncaught--- exception. An EOF (the frame never completing) gets libpq's own wording for--- it; any other handshake-time I\/O error falls back to its 'show'n form.+-- exception. A connection lost outright - a clean EOF or a hard TCP reset -+-- gets libpq's own wording for it (see 'connectionLostMessage'); any other+-- handshake-time I\/O error falls back to its 'show'n form. handshakeFailureMessage :: Connection -> ConnInfo -> IOException -> IO ByteString handshakeFailureMessage connection connInfo err = do   let fmtFields-        | isEOFError err =-            "server closed the connection unexpectedly\n\tThis probably means the server terminated abnormally\n\tbefore or while processing the request.\n"-        | otherwise = ByteString.Char8.pack (show err)+        | isConnectionLost err = connectionLostMessage err <> "\n"+        | otherwise = connectionLostMessage err   if Transport.isUnixSocketHost (host connInfo)     then pure (unixSocketFailureMessage connInfo fmtFields)     else tcpFailureMessage connection connInfo fmtFields++-- | Whether an 'IOException' represents the connection being lost outright -+-- a clean EOF (the frame never completing) or a hard TCP reset+-- (@ECONNRESET@, which 'Network.Socket' surfaces as 'ResourceVanished'+-- rather than 'System.IO.Error.eofErrorType') - as opposed to some other kind+-- of I\/O failure.+isConnectionLost :: IOException -> Bool+isConnectionLost err = isEOFError err || ioe_type err == ResourceVanished++-- | libpq's own wording for a connection lost outright (see+-- 'isConnectionLost'), without a trailing newline - callers that embed this+-- inline (e.g. 'handshakeFailureMessage') append one themselves; the one that+-- folds it into a result's error fields ('Pqi.Native.Query') gets its+-- trailing newline for free from 'Pqi.Native.Types.formatResultError'. Falls+-- back to the exception's 'show'n form for anything else.+connectionLostMessage :: IOException -> ByteString+connectionLostMessage err+  | 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)  -- | 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
@@ -17,7 +17,7 @@   ) where -import Control.Exception (mask_)+import Control.Exception (IOException, catch, mask_) import qualified Data.Map.Strict as Map import qualified Data.Sequence as Seq import Pqi (ConnStatus (..), ExecStatus (..), Format (..), PipelineStatus (..))@@ -78,7 +78,7 @@ exec :: Connection -> ByteString -> IO (Maybe NativeResult) exec connection sql = withReady connection do   sendMessage connection (queryMessage sql)-  lastMaybe <$> collectSimple connection sql+  catch (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)@@ -86,7 +86,7 @@   | tooManyParams params = pure Nothing   | otherwise = withReady connection do       sendMessage connection (paramsWrite sql params resultFormat)-      Just <$> collectExtended connection sql+      catch (Just <$> collectExtended connection sql) (fmap Just . connectionLostResult connection sql)  -- | Prepare a named statement. prepare :: Connection -> ByteString -> ByteString -> Maybe [Word32] -> IO (Maybe NativeResult)@@ -94,7 +94,7 @@   | maybe False tooManyParams parameterTypes = pure Nothing   | otherwise = withReady connection do       sendMessage connection (prepareWrite name sql parameterTypes)-      Just <$> collectExtended connection sql+      catch (Just <$> collectExtended connection sql) (fmap Just . connectionLostResult connection sql)  -- | Execute a previously prepared statement. execPrepared :: Connection -> ByteString -> [Maybe (ByteString, Format)] -> Format -> IO (Maybe NativeResult)@@ -102,7 +102,7 @@   | tooManyParams params = pure Nothing   | otherwise = withReady connection do       sendMessage connection (preparedWrite name params resultFormat)-      Just <$> collectExtended connection ""+      catch (Just <$> collectExtended connection "") (fmap Just . connectionLostResult connection "")  -- * Asynchronous flows @@ -381,13 +381,13 @@ describePrepared :: Connection -> ByteString -> IO (Maybe NativeResult) describePrepared connection name = withReady connection do   sendMessage connection (describeStatementMessage name <> syncMessage)-  Just <$> collectExtended connection ""+  catch (Just <$> collectExtended connection "") (fmap Just . connectionLostResult connection "")  -- | Describe a portal. describePortal :: Connection -> ByteString -> IO (Maybe NativeResult) describePortal connection name = withReady connection do   sendMessage connection (describePortalMessage name <> syncMessage)-  Just <$> collectExtended connection ""+  catch (Just <$> collectExtended connection "") (fmap Just . connectionLostResult connection "")  -- * Parameter projections @@ -423,6 +423,18 @@   case status of     ConnectionOk -> action     _ -> pure Nothing++-- | Turn a read loop's escaped 'IOException' - e.g. a connection reset while+-- a result is still in flight - into a classified 'FatalError' result,+-- matching @PQexec@: libpq never throws here, it reports the same+-- "server closed the connection unexpectedly" wording it uses for a+-- handshake-time loss (see 'connectionLostMessage'), and marks the+-- 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")+  pure (NativeResult FatalError [] [] Nothing (Map.singleton 0x4d message) [] sql)  -- accumulator for a result under construction data Builder = Builder
src/transport/Pqi/Native/Transport.hs view
@@ -104,8 +104,8 @@ -- as needed. Throws on EOF before @n@ bytes are available. -- -- The wait for bytes is deliberately left interruptible. Nothing has been--- consumed at this point, so a caller that gives up here loses nothing - and,--- crucially, is /able/ to give up. A caller blocked on a message the server+-- consumed at this point, so a caller that gives up here loses nothing - and+-- is /able/ to give up. A caller blocked on a message the server -- will never send (an aborted pipeline whose bookkeeping has drifted, say) -- must stay abandonable by 'System.Timeout.timeout'; masking the wait -- uninterruptibly turns that stall into a deadlock no timer can break.