pg-wire-0.1.0.0: src/PgWire/Async.hs
{-# OPTIONS_GHC -fno-full-laziness #-}
-- | Asynchronous sender/receiver split for PostgreSQL connections.
--
-- __Audience:__ this module is exposed for downstream library authors
-- (the @valiant@ runtime, custom adapters). Most application code
-- should reach for "PgWire.Connection" instead. The types and
-- functions here are public and stable, but they sit below the
-- request/response abstraction and require knowledge of the v3 wire
-- protocol to use safely.
--
-- Each connection spawns two green threads after startup:
--
-- * __Writer thread__ — drains the send queue, batches messages via
-- vectored I/O ('sendMany'), and enqueues response MVars.
-- * __Reader thread__ — parses backend messages from the socket and
-- fills MVars in FIFO order.
--
-- Application threads submit 'Request's and block on the returned 'MVar'.
-- PostgreSQL processes messages in strict FIFO order, so the i-th response
-- always corresponds to the i-th pending request — no correlation IDs needed.
--
-- This architecture enables automatic pipelining: when N threads query
-- concurrently, their messages are coalesced into a single 'sendMany'
-- syscall by the writer, and responses are demultiplexed by the reader.
module PgWire.Async
( -- * Types
AsyncWireConn (..)
, Request (..)
, Response (..)
, ResponseCollector (..)
-- * Lifecycle
, spawnAsyncWireConn
, shutdownAsyncWireConn
-- * Submitting requests
, submitRequest
, submitExclusive
) where
import Control.Concurrent (forkIO)
import Control.Concurrent.Async (Async, async, cancel, link2)
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Exception (SomeException, catch, mask, onException, throwIO, try)
import Control.Monad (void)
import Data.ByteString (ByteString)
import Data.ByteString.Char8 qualified as BS8
import Data.IORef
import Data.Int (Int32, Int64)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Vector (Vector)
import Data.Vector qualified as V
import Data.Vector.Mutable qualified as VM
import PgWire.Error (PgWireError (..), throwPgWire)
import PgWire.Protocol.Backend
import PgWire.Protocol.Frontend (FrontendMsg (..))
import PgWire.Wire (WireConn, recvBackendMsg, sendFrontendMsgs)
------------------------------------------------------------------------
-- Types
------------------------------------------------------------------------
-- | What application threads submit to the async connection.
data Request
= -- | Extended query: pre-built messages (Bind+Execute+Sync) with a collector.
ReqExtendedQuery ![FrontendMsg] !ResponseCollector
| -- | Simple text query.
ReqSimpleQuery !ByteString
| -- | Parse a prepared statement (Parse+Sync).
ReqPrepare !FrontendMsg
| -- | Close a prepared statement (Close+Sync).
ReqClose !FrontendMsg
-- | What comes back to the caller via MVar.
data Response
= -- | Rows collected from DataRow messages.
RespRows ![Vector (Maybe ByteString)]
| -- | Command tag from CommandComplete.
RespCommand !CommandTag
| -- | Simple query result: rows + optional tag.
RespSimple ![[Maybe ByteString]] !(Maybe CommandTag)
| -- | ParseComplete acknowledged.
RespParsed
| -- | CloseComplete acknowledged.
RespClosed
| -- | Multiple sub-results (one per Bind+Execute in a batch).
RespBatchRows ![[Vector (Maybe ByteString)]]
| -- | Batch command result: total rows affected.
RespBatchCommand !Int64
| -- | Rows collected from DataRow messages AND the command tag.
RespRowsAndCommand ![Vector (Maybe ByteString)] !CommandTag
| -- | Rows collected as a Vector (not a list). Used by fetchAllVec.
RespRowsVec !(Vector (Vector (Maybe ByteString)))
| -- | At most one row. Used by fetchOne/fetchScalar/fetchExists.
RespFirstRow !(Maybe (Vector (Maybe ByteString)))
-- | Tells the reader thread how to interpret backend messages for a request.
data ResponseCollector
= -- | Collect DataRow until CommandComplete, then ReadyForQuery.
CollectRows
| -- | Expect CommandComplete + ReadyForQuery, extract row count.
CollectCommand
| -- | Expect ParseComplete + ReadyForQuery.
CollectParseComplete
| -- | Expect CloseComplete + ReadyForQuery.
CollectCloseComplete
| -- | Collect N sub-results (Bind+Execute repeated N times), one ReadyForQuery at end.
CollectBatch !Int
| -- | Collect N command sub-results, one ReadyForQuery at end.
CollectBatchCommand !Int
| -- | Collect DataRow until CommandComplete, returning both rows and the command tag.
CollectRowsAndCommand
| -- | Like CollectRows but collects into a Vector instead of a list.
CollectRowsVec
| -- | Collect at most the first row. Discards remaining DataRows.
CollectFirstRow
| -- | Simple query protocol: text rows + optional tag + ReadyForQuery.
CollectSimple
-- | Enqueued by the writer, dequeued by the reader.
data PendingResponse = PendingResponse
{ prCollector :: !ResponseCollector
, prMVar :: !(MVar (Either PgWireError Response))
}
-- | The async connection state. Owns the writer and reader threads.
data AsyncWireConn = AsyncWireConn
{ awcWire :: !WireConn
, awcSendQueue :: !(TBQueue (Request, MVar (Either PgWireError Response)))
, awcPending :: !(TQueue PendingResponse)
, awcExclusive :: !(TMVar (MVar (Either PgWireError Response)))
-- ^ When set, the writer pauses the pipeline for exclusive access.
, awcAlive :: !(TVar Bool)
, awcTxStatus :: !(IORef TxStatus)
, awcNotifyHandler :: !(IORef (Int32 -> ByteString -> ByteString -> IO ()))
, awcNoticeHandler :: !(IORef (PgNotice -> IO ()))
, awcParamStatus :: !(IORef (Map ByteString ByteString))
, awcWriterThread :: !(Async ())
, awcReaderThread :: !(Async ())
, awcSendLock :: !(TMVar ())
-- ^ Fast-path send lock. When the writer is idle, a caller can take
-- this lock and send directly on the wire, bypassing the TBQueue.
-- The reader still collects the response normally via the pending queue.
}
-- | Shared state between writer/reader threads. Doesn't include the
-- thread handles themselves (avoids circular dependency with StrictData).
data AsyncCore = AsyncCore
{ acWire :: !WireConn
, acSendQueue :: !(TBQueue (Request, MVar (Either PgWireError Response)))
, acPending :: !(TQueue PendingResponse)
, acExclusive :: !(TMVar (MVar (Either PgWireError Response)))
, acAlive :: !(TVar Bool)
, acTxStatus :: !(IORef TxStatus)
, acNotifyHandler :: !(IORef (Int32 -> ByteString -> ByteString -> IO ()))
, acNoticeHandler :: !(IORef (PgNotice -> IO ()))
, acParamStatus :: !(IORef (Map ByteString ByteString))
, acSendLock :: !(TMVar ())
}
------------------------------------------------------------------------
-- Lifecycle
------------------------------------------------------------------------
-- | Spawn writer and reader threads for an established connection.
-- Call this after the serial startup handshake completes.
spawnAsyncWireConn
:: WireConn
-> IORef TxStatus
-> IORef (Map ByteString ByteString)
-> IO AsyncWireConn
spawnAsyncWireConn wc txRef paramRef = do
sendQueue <- newTBQueueIO 64
pending <- newTQueueIO
exclusive <- newEmptyTMVarIO
alive <- newTVarIO True
sendLock <- newTMVarIO () -- initially available
notifyHandler <- newIORef (\_ _ _ -> pure ())
noticeHandler <- newIORef (\_ -> pure ())
-- The writer and reader threads need access to the shared STM/IORef state
-- but NOT to each other's Async handles. We build a "core" with just the
-- shared state, spawn threads from that, then assemble the final record.
let core = AsyncCore
{ acWire = wc
, acSendQueue = sendQueue
, acPending = pending
, acExclusive = exclusive
, acAlive = alive
, acTxStatus = txRef
, acNotifyHandler = notifyHandler
, acNoticeHandler = noticeHandler
, acParamStatus = paramRef
, acSendLock = sendLock
}
writer <- async (writerThread core)
reader <- async (readerThread core)
-- Link threads: if either dies, both die.
link2 writer reader
pure AsyncWireConn
{ awcWire = wc
, awcSendQueue = sendQueue
, awcPending = pending
, awcExclusive = exclusive
, awcAlive = alive
, awcTxStatus = txRef
, awcNotifyHandler = notifyHandler
, awcNoticeHandler = noticeHandler
, awcParamStatus = paramRef
, awcWriterThread = writer
, awcReaderThread = reader
, awcSendLock = sendLock
}
-- | Shut down the async connection. Signals threads to stop and cancels them.
shutdownAsyncWireConn :: AsyncWireConn -> IO ()
shutdownAsyncWireConn awc = do
atomically $ writeTVar (awcAlive awc) False
cancel (awcWriterThread awc) `catch` \(_ :: SomeException) -> pure ()
cancel (awcReaderThread awc) `catch` \(_ :: SomeException) -> pure ()
drainPendingWithError awc ConnectionDead
------------------------------------------------------------------------
-- Submitting requests
------------------------------------------------------------------------
-- | Submit a request and block until the response arrives.
-- Throws 'ConnectionDead' if the async threads have died.
-- | Submit a request and block until the response arrives.
-- Fast path: if the send lock is available (no contention), send directly
-- on the wire without going through the writer thread's TBQueue. The reader
-- still collects the response normally. This eliminates ~2-4μs of thread
-- coordination overhead for single-threaded workloads.
-- Slow path: enqueue to the writer thread (enables automatic pipelining
-- under concurrency).
submitRequest :: AsyncWireConn -> Request -> IO Response
submitRequest awc req = do
respVar <- newEmptyMVar
-- Try fast path: take the send lock and send directly.
-- If the writer is busy (lock unavailable), fall back to the queue.
path <- atomically $ do
a <- readTVar (awcAlive awc)
if not a
then pure Nothing -- dead
else do
mLock <- tryTakeTMVar (awcSendLock awc)
case mLock of
Just () -> pure (Just True) -- fast path: we have the lock
Nothing -> do
writeTBQueue (awcSendQueue awc) (req, respVar)
pure (Just False) -- slow path: queued
case path of
Nothing -> throwPgWire ConnectionDead
Just False -> do
-- Slow path: wait for response from reader via writer
result <- takeMVar respVar
case result of
Left err -> throwIO err
Right resp -> pure resp
Just True -> do
-- Fast path: send directly, enqueue pending for reader
let (msgs, collector) = requestToMsgs req
releaseLock = atomically $ putTMVar (awcSendLock awc) ()
sendFrontendMsgs (awcWire awc) msgs
`onException` releaseLock
-- Enqueue pending and release lock atomically so another fast-path
-- caller cannot send and enqueue before our pending is visible to
-- the reader — preserving the FIFO invariant.
atomically $ do
writeTQueue (awcPending awc) (PendingResponse collector respVar)
putTMVar (awcSendLock awc) ()
-- Wait for response from reader
result <- takeMVar respVar
case result of
Left err -> throwIO err
Right resp -> pure resp
-- | Submit an exclusive request. The writer thread will:
-- 1. Stop accepting new requests
-- 2. Wait for all pending responses to be collected by the reader
-- 3. Hand control to the caller
-- 4. Resume normal operation when the caller finishes
--
-- Used for COPY, cursors, folds — operations that need direct socket access.
submitExclusive :: AsyncWireConn -> (WireConn -> IORef TxStatus -> IO a) -> IO a
submitExclusive awc action = mask $ \restore -> do
respVar <- newEmptyMVar
-- Combine alive check with exclusive signal in one STM transaction.
signaled <- atomically $ do
a <- readTVar (awcAlive awc)
if a
then putTMVar (awcExclusive awc) respVar >> pure True
else pure False
if not signaled
then throwPgWire ConnectionDead
else do
-- Wait for writer to drain pending and hand us control
result <- takeMVar respVar
case result of
Left err -> throwIO err
Right _ -> do
-- We now have exclusive access to the WireConn.
-- Reader is blocked on empty awcPending.
-- Writer is blocked waiting for us to signal done.
let signalDone = do
doneVar <- newEmptyMVar
atomically $ putTMVar (awcExclusive awc) doneVar
putMVar doneVar (Right RespParsed)
a <- restore (action (awcWire awc) (awcTxStatus awc))
`onException` signalDone
signalDone
pure a
-- | Extract messages and collector from a request (used by fast path).
requestToMsgs :: Request -> ([FrontendMsg], ResponseCollector)
requestToMsgs (ReqExtendedQuery msgs collector) = (msgs, collector)
requestToMsgs (ReqSimpleQuery sql) = ([Query sql], CollectSimple)
requestToMsgs (ReqPrepare parseMsg) = ([parseMsg, Flush], CollectParseComplete)
requestToMsgs (ReqClose closeMsg) = ([closeMsg, Sync], CollectCloseComplete)
------------------------------------------------------------------------
-- Writer thread
------------------------------------------------------------------------
data WriterAction
= WriterExclusive !(MVar (Either PgWireError Response))
| WriterBatch ![(Request, MVar (Either PgWireError Response))]
writerThread :: AsyncCore -> IO ()
writerThread ac = go `catch` onDeath
where
go :: IO ()
go = do
action <- atomically $ tryExclusive `orElse` drainQueue
case action of
WriterExclusive respVar -> handleExclusive respVar
WriterBatch items -> handleBatch items
go
tryExclusive :: STM WriterAction
tryExclusive = do
respVar <- takeTMVar (acExclusive ac)
pure (WriterExclusive respVar)
drainQueue :: STM WriterAction
drainQueue = do
first <- readTBQueue (acSendQueue ac)
-- Take the send lock so fast-path callers can't send while we're sending.
takeTMVar (acSendLock ac)
rest <- drainTBQueue (acSendQueue ac)
pure (WriterBatch (first : rest))
handleBatch :: [(Request, MVar (Either PgWireError Response))] -> IO ()
handleBatch items = do
let (allMsgs, pendings) = unzip (map buildItem items)
atomically $ mapM_ (writeTQueue (acPending ac)) pendings
sendFrontendMsgs (acWire ac) (concat allMsgs)
-- Release the send lock so fast-path callers can send again.
atomically $ putTMVar (acSendLock ac) ()
buildItem :: (Request, MVar (Either PgWireError Response)) -> ([FrontendMsg], PendingResponse)
buildItem (req, respVar) = case req of
ReqExtendedQuery msgs collector ->
(msgs, PendingResponse collector respVar)
ReqSimpleQuery sql ->
([Query sql], PendingResponse CollectSimple respVar)
ReqPrepare parseMsg ->
([parseMsg, Flush], PendingResponse CollectParseComplete respVar)
ReqClose closeMsg ->
([closeMsg, Sync], PendingResponse CollectCloseComplete respVar)
handleExclusive :: MVar (Either PgWireError Response) -> IO ()
handleExclusive respVar = do
atomically $ do
empty <- isEmptyTQueue (acPending ac)
check empty
putMVar respVar (Right RespParsed)
doneVar <- atomically $ takeTMVar (acExclusive ac)
_ <- takeMVar doneVar
pure ()
onDeath :: SomeException -> IO ()
onDeath _ = do
atomically $ writeTVar (acAlive ac) False
drainPendingWithErrorCore ac ConnectionDead
------------------------------------------------------------------------
-- Reader thread
------------------------------------------------------------------------
readerThread :: AsyncCore -> IO ()
readerThread ac = go `catch` onDeath
where
go :: IO ()
go = do
pr <- atomically $ readTQueue (acPending ac)
-- Catch query errors so they don't kill the reader thread.
-- A QueryError is per-request, not connection-fatal.
result <- try (collectResponse (prCollector pr))
case result of
Right resp -> putMVar (prMVar pr) (Right resp)
Left (err :: PgWireError) -> do
-- After ErrorResponse with Sync, Postgres sends ReadyForQuery.
-- After ErrorResponse with Flush (preparation), it does NOT.
case prCollector pr of
CollectParseComplete -> pure ()
_ -> drainUntilReady
putMVar (prMVar pr) (Left err)
go
-- Drain messages until ReadyForQuery (error recovery).
drainUntilReady :: IO ()
drainUntilReady = do
msg <- recvAndDispatch
case msg of
ReadyForQuery status -> writeIORef (acTxStatus ac) status
_ -> drainUntilReady
collectResponse :: ResponseCollector -> IO Response
collectResponse CollectRows = do
rows <- collectRowsLoop
waitReadyForQuery
pure (RespRows rows)
collectResponse CollectCommand = do
tag <- collectCommandLoop
waitReadyForQuery
pure (RespCommand tag)
collectResponse CollectParseComplete = do
waitParseCompleteLoop
-- No waitReadyForQuery: Flush doesn't trigger ReadyForQuery.
pure RespParsed
collectResponse CollectCloseComplete = do
waitCloseCompleteLoop
waitReadyForQuery
pure RespClosed
collectResponse CollectRowsAndCommand = do
(rows, tag) <- collectRowsAndCommandLoop
waitReadyForQuery
pure (RespRowsAndCommand rows tag)
collectResponse CollectRowsVec = do
vec <- collectRowsVecLoop
waitReadyForQuery
pure (RespRowsVec vec)
collectResponse CollectFirstRow = do
mRow <- collectFirstRowLoop
waitReadyForQuery
pure (RespFirstRow mRow)
collectResponse (CollectBatch n) = do
results <- collectBatchLoop n
waitReadyForQuery
pure (RespBatchRows results)
collectResponse (CollectBatchCommand n) = do
total <- collectBatchCommandLoop n 0
waitReadyForQuery
pure (RespBatchCommand total)
collectResponse CollectSimple = do
(rows, tag) <- collectSimpleLoop [] Nothing
pure (RespSimple rows tag)
-- Collectors skip both ParseComplete and BindComplete so that
-- Parse+Bind+Execute can be coalesced into a single request.
collectRowsLoop :: IO [Vector (Maybe ByteString)]
collectRowsLoop = loop id
where
loop !acc = do
msg <- recvAndDispatch
case msg of
ParseComplete -> loop acc
BindComplete -> loop acc
DataRow vals -> loop (acc . (vals :))
CommandComplete _ -> pure (acc [])
EmptyQueryResponse -> pure (acc [])
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in rows: " <> BS8.pack (show other)))
-- | Collect rows into a growable mutable vector. Starts at capacity 64,
-- doubles when full. Final freeze+slice produces an exact-size immutable Vector.
collectRowsVecLoop :: IO (Vector (Vector (Maybe ByteString)))
collectRowsVecLoop = do
mv <- VM.new 64
(finalMv, !n) <- loop mv 0
V.unsafeFreeze (VM.slice 0 n finalMv)
where
loop !mv !i = do
msg <- recvAndDispatch
case msg of
ParseComplete -> loop mv i
BindComplete -> loop mv i
DataRow vals -> do
mv' <- if i >= VM.length mv
then VM.grow mv (VM.length mv) -- double capacity
else pure mv
VM.write mv' i vals
loop mv' (i + 1)
CommandComplete _ -> pure (mv, i)
EmptyQueryResponse -> pure (mv, i)
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in rows vec: " <> BS8.pack (show other)))
-- | Collect at most the first row, discard the rest.
collectFirstRowLoop :: IO (Maybe (Vector (Maybe ByteString)))
collectFirstRowLoop = loop
where
loop = do
msg <- recvAndDispatch
case msg of
ParseComplete -> loop
BindComplete -> loop
DataRow vals -> do
-- Got first row, now drain remaining rows
drainRows
pure (Just vals)
CommandComplete _ -> pure Nothing
EmptyQueryResponse -> pure Nothing
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in first row: " <> BS8.pack (show other)))
drainRows = do
msg <- recvAndDispatch
case msg of
DataRow _ -> drainRows -- discard
CommandComplete _ -> pure ()
EmptyQueryResponse -> pure ()
NoticeResponse _ -> drainRows
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected draining rows: " <> BS8.pack (show other)))
collectRowsAndCommandLoop :: IO ([Vector (Maybe ByteString)], CommandTag)
collectRowsAndCommandLoop = loop id
where
loop !acc = do
msg <- recvAndDispatch
case msg of
ParseComplete -> loop acc
BindComplete -> loop acc
DataRow vals -> loop (acc . (vals :))
CommandComplete tag -> pure (acc [], tag)
EmptyQueryResponse -> pure (acc [], OtherTag "EMPTY")
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in rows+command: " <> BS8.pack (show other)))
collectCommandLoop :: IO CommandTag
collectCommandLoop = do
msg <- recvAndDispatch
case msg of
ParseComplete -> collectCommandLoop
BindComplete -> collectCommandLoop
CommandComplete tag -> pure tag
EmptyQueryResponse -> pure (OtherTag "EMPTY")
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in command: " <> BS8.pack (show other)))
waitParseCompleteLoop :: IO ()
waitParseCompleteLoop = do
msg <- recvAndDispatch
case msg of
ParseComplete -> pure ()
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Expected ParseComplete, got: " <> BS8.pack (show other)))
waitCloseCompleteLoop :: IO ()
waitCloseCompleteLoop = do
msg <- recvAndDispatch
case msg of
CloseComplete -> pure ()
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Expected CloseComplete, got: " <> BS8.pack (show other)))
collectBatchLoop :: Int -> IO [[Vector (Maybe ByteString)]]
collectBatchLoop 0 = pure []
collectBatchLoop n = do
rows <- collectRowsLoop
rest <- collectBatchLoop (n - 1)
pure (rows : rest)
collectBatchCommandLoop :: Int -> Int64 -> IO Int64
collectBatchCommandLoop 0 !total = pure total
collectBatchCommandLoop n !total = do
msg <- recvAndDispatch
case msg of
ParseComplete -> collectBatchCommandLoop n total
BindComplete -> collectBatchCommandLoop n total
CommandComplete tag -> collectBatchCommandLoop (n - 1) (total + tagRows tag)
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in batch cmd: " <> BS8.pack (show other)))
collectSimpleLoop :: [[Maybe ByteString]] -> Maybe CommandTag -> IO ([[Maybe ByteString]], Maybe CommandTag)
collectSimpleLoop !rows !tag = do
msg <- recvAndDispatch
case msg of
RowDescription _ -> collectSimpleLoop rows tag
DataRow vals -> collectSimpleLoop (V.toList vals : rows) tag
CommandComplete ct -> collectSimpleLoop rows (Just ct)
EmptyQueryResponse -> collectSimpleLoop rows tag
ReadyForQuery status -> do
writeIORef (acTxStatus ac) status
pure (reverse rows, tag)
ErrorResponse err -> throwIO (QueryError err)
other -> throwIO (ProtocolError ("Unexpected in simple query: " <> BS8.pack (show other)))
waitReadyForQuery :: IO ()
waitReadyForQuery = do
msg <- recvAndDispatch
case msg of
ReadyForQuery status -> writeIORef (acTxStatus ac) status
_ -> waitReadyForQuery
recvAndDispatch :: IO BackendMsg
recvAndDispatch = do
msg <- recvBackendMsg (acWire ac)
case msg of
NotificationResponse pid channel payload -> do
handler <- readIORef (acNotifyHandler ac)
_ <- forkIO (handler pid channel payload `catch` \(_ :: SomeException) -> pure ())
recvAndDispatch
NoticeResponse notice -> do
handler <- readIORef (acNoticeHandler ac)
_ <- forkIO (handler notice `catch` \(_ :: SomeException) -> pure ())
recvAndDispatch
ParameterStatus key value -> do
modifyIORef' (acParamStatus ac) (Map.insert key value)
recvAndDispatch
_ -> pure msg
onDeath :: SomeException -> IO ()
onDeath _ = do
atomically $ writeTVar (acAlive ac) False
drainPendingWithErrorCore ac ConnectionDead
------------------------------------------------------------------------
-- Helpers
------------------------------------------------------------------------
drainTBQueue :: TBQueue a -> STM [a]
drainTBQueue q = loop id
where
loop acc = do
mItem <- tryReadTBQueue q
case mItem of
Just item -> loop (acc . (item :))
Nothing -> pure (acc [])
drainPendingWithError :: AsyncWireConn -> PgWireError -> IO ()
drainPendingWithError awc err = do
pendings <- atomically $ flushTQueue (awcPending awc)
mapM_ (\pr -> void (tryPutMVar (prMVar pr) (Left err))) pendings
queued <- atomically $ drainTBQueue (awcSendQueue awc)
mapM_ (\(_, mv) -> void (tryPutMVar mv (Left err))) queued
mExcl <- atomically $ tryTakeTMVar (awcExclusive awc)
case mExcl of
Just mv -> void (tryPutMVar mv (Left err))
Nothing -> pure ()
-- | Like 'drainPendingWithError' but operates on 'AsyncCore' (used by threads).
drainPendingWithErrorCore :: AsyncCore -> PgWireError -> IO ()
drainPendingWithErrorCore ac err = do
pendings <- atomically $ flushTQueue (acPending ac)
mapM_ (\pr -> void (tryPutMVar (prMVar pr) (Left err))) pendings
queued <- atomically $ drainTBQueue (acSendQueue ac)
mapM_ (\(_, mv) -> void (tryPutMVar mv (Left err))) queued
mExcl <- atomically $ tryTakeTMVar (acExclusive ac)
case mExcl of
Just mv -> void (tryPutMVar mv (Left err))
Nothing -> pure ()
tagRows :: CommandTag -> Int64
tagRows (InsertTag n) = n
tagRows (UpdateTag n) = n
tagRows (DeleteTag n) = n
tagRows (SelectTag n) = n
tagRows (OtherTag _) = 0