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 +11/−1
- pqi-native.cabal +3/−3
- src/library/Pqi/Native/Connection.hs +41/−7
- src/library/Pqi/Native/Query.hs +19/−7
- src/transport/Pqi/Native/Transport.hs +2/−2
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.