packages feed

amqp 0.12.3 → 0.13.0

raw patch · 4 files changed

+135/−10 lines, 4 filesdep +stmPVP ok

version bump matches the API change (PVP)

Dependencies added: stm

API changes (from Hackage documentation)

+ Network.AMQP: Complete :: (IntSet, IntSet) -> ConfirmationResult
+ Network.AMQP: Partial :: (IntSet, IntSet, IntSet) -> ConfirmationResult
+ Network.AMQP: addConfirmationListener :: Channel -> ((Word64, Bool, AckType) -> IO ()) -> IO ()
+ Network.AMQP: confirmSelect :: Channel -> Bool -> IO ()
+ Network.AMQP: data ConfirmationResult
+ Network.AMQP: waitForConfirms :: Channel -> IO ConfirmationResult
+ Network.AMQP: waitForConfirmsUntil :: Channel -> Int -> IO ConfirmationResult
+ Network.AMQP.Types: Complete :: (IntSet, IntSet) -> ConfirmationResult
+ Network.AMQP.Types: Partial :: (IntSet, IntSet, IntSet) -> ConfirmationResult
+ Network.AMQP.Types: data ConfirmationResult
+ Network.AMQP.Types: instance Show ConfirmationResult

Files

Network/AMQP.hs view
@@ -113,6 +113,13 @@     txCommit,
     txRollback,
 
+    -- * Confirms
+    confirmSelect,
+    waitForConfirms,
+    waitForConfirmsUntil,
+    addConfirmationListener,
+    ConfirmationResult(..),
+
     -- * Flow Control
     flow,
 
@@ -129,7 +136,11 @@     fromURI
 ) where
 
+import Control.Applicative
 import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Monad(when)
+import Data.IORef
 import Data.List.Split (splitOn)
 import Data.Binary
 import Data.Binary.Put
@@ -139,6 +150,7 @@ 
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.ByteString as BS
+import qualified Data.IntSet as IntSet
 import qualified Data.Text.Encoding as E
 import qualified Data.Map as M
 import qualified Data.Text as T
@@ -413,7 +425,10 @@             (fmap ShortString $ msgClusterID msg)
             )
             (msgBody msg))
-    return ()
+    nxtSeqNum <- readIORef (nextPublishSeqNum chan)
+    when (nxtSeqNum /= 0) $ do
+      writeIORef (nextPublishSeqNum chan) $ succ nxtSeqNum
+      atomically $ modifyTVar' (unconfirmedSet chan) $ \uSet -> IntSet.insert nxtSeqNum uSet
 
 -- | @getMsg chan ack queue@ gets a message from the specified queue. If @ack=='Ack'@, you have to call 'ackMsg' or 'ackEnv' for any message that you get, otherwise it might be delivered again in the future (by calling 'recoverMsgs')
 getMsg :: Channel -> Ack -> Text -> IO (Maybe (Message, Envelope))
@@ -491,6 +506,61 @@ txRollback chan = do
     (SimpleMethod Tx_rollback_ok) <- request chan $ SimpleMethod Tx_rollback
     return ()
+
+
+-------------------- PUBLISHER CONFIRMS ------------------------
+
+{- | @confirmSelect chan nowait@ puts the channel in publisher confirm mode. This mode is a RabbitMQ
+    extension where a producer receives confirmations when messages are successfully processed by
+    the broker. Publisher confirms are a relatively lightweight alternative to full transactional
+    mode. For details about the delivery guarantees and performace implications of this mode, see
+    https://www.rabbitmq.com/confirms.html. Note that on a single channel, publisher confirms and
+    transactions are mutually exclusive (you cannot select both at the same time).
+    When @nowait==True@ the server will not send back a response to this method.
+-}
+confirmSelect :: Channel -> Bool -> IO ()
+confirmSelect chan nowait = do
+    _ <- modifyIORef' (nextPublishSeqNum chan) $ \seqn -> if seqn == 0 then 1 else seqn
+    if nowait
+        then writeAssembly chan $ SimpleMethod (Confirm_select True)
+        else do
+            (SimpleMethod Confirm_select_ok) <- request chan $ SimpleMethod (Confirm_select False)
+            return ()
+
+{- | Calling this function will cause the invoking thread to block until all previously published
+messages have been acknowledged by the broker (positively or negatively). Returns a value of type
+@ConfirmationResult, holding a tuple of two @IntSet's @(acked, nacked), ontaining the delivery tags
+for the messages that have been confirmed by the broker.
+-}
+waitForConfirms :: Channel -> IO ConfirmationResult
+waitForConfirms chan = atomically $ Complete <$> waitForAllConfirms chan
+
+{- | Same as @waitForConfirms, but with a timeout in microseconds. Note that, since this operation
+may timeout before the server has acked or nacked all pending messages, the returned @ConfirmationResult
+should be pattern-matched for the constructors @Complete (acked, nacked) and @Partial (acked, nacked, pending)
+-}
+waitForConfirmsUntil :: Channel -> Int -> IO ConfirmationResult
+waitForConfirmsUntil chan timeout = do
+  delay <- registerDelay timeout
+  let partial = do
+        expired <- readTVar delay
+        if expired
+           then return . Partial =<< (,,)
+                <$> swapTVar (ackedSet chan) IntSet.empty
+                <*> swapTVar (nackedSet chan) IntSet.empty
+                <*> readTVar (unconfirmedSet chan)
+           else retry
+      complete = return . Complete =<< waitForAllConfirms chan
+  atomically $ complete `orElse` partial
+
+{- | Adds a handler which will be invoked each time the @Channel receives a confirmation from the broker.
+--The parameters passed to the the handler are the @deliveryTag for the message being confirmed, a flag
+indicating whether the confirmation refers to this message individually (@False) or all messages up to
+this one (@True) and an @AckType whose value can be either @BasicAck or @BasicNack.
+-}
+addConfirmationListener :: Channel -> ((Word64, Bool, AckType) -> IO ()) -> IO ()
+addConfirmationListener chan handler =
+    modifyMVar_ (confirmListeners chan) $ \listeners -> return $ handler:listeners
 
 --------------------- FLOW CONTROL ------------------------
 
Network/AMQP/Internal.hs view
@@ -4,12 +4,15 @@ import Paths_amqp(version)
 import Data.Version(showVersion)
 
+import Control.Applicative
 import Control.Concurrent
+import Control.Concurrent.STM
 import Control.Monad
 import Data.Binary
 import Data.Binary.Get
 import Data.Binary.Put as BPut
 import Data.Int (Int64)
+import Data.IORef
 import Data.Maybe
 import Data.Text (Text)
 import Network
@@ -22,6 +25,7 @@ import qualified Data.Map as M
 import qualified Data.Foldable as F
 import qualified Data.IntMap as IM
+import qualified Data.IntSet as IntSet
 import qualified Data.Sequence as Seq
 import qualified Data.Text as T
 import qualified Data.Text.Encoding as E
@@ -33,6 +37,7 @@ import Network.AMQP.Generated
 import Network.AMQP.ChannelAllocator
 
+data AckType = BasicAck | BasicNack deriving Show
 
 data DeliveryMode = Persistent -- ^ the message will survive server restarts (if the queue is durable)
                   | NonPersistent -- ^ the message may be lost after server restarts
@@ -362,13 +367,13 @@ 
     skippedBeatEx = ConnectionClosedException "killed connection after missing 2 consecutive heartbeats"
 
-    checkReceiveTimeout = check (connLastReceived conn) receiveTimeout
+    checkReceiveTimeout = doCheck (connLastReceived conn) receiveTimeout
         (killConnection conn (CE.toException skippedBeatEx) connThread)
 
-    checkSendTimeout = check (connLastSent conn) sendTimeout
+    checkSendTimeout = doCheck (connLastSent conn) sendTimeout
         (writeFrame (connHandle conn) (Frame 0 HeartbeatPayload))
 
-    check var timeout_µs action = withMVar var $ \lastFrameTime -> do
+    doCheck var timeout_µs action = withMVar var $ \lastFrameTime -> do
         time <- getTimestamp
         when (time >= lastFrameTime + timeout_µs) $ do
             action
@@ -468,10 +473,16 @@                     channelID :: Word16,
                     lastConsumerTag :: MVar Int,
 
+                    nextPublishSeqNum :: IORef Int,
+                    unconfirmedSet :: TVar IntSet.IntSet,
+                    ackedSet :: TVar IntSet.IntSet,  --delivery tags
+                    nackedSet :: TVar IntSet.IntSet, --accumulate here.
+
                     chanActive :: Lock, -- used for flow-control. if lock is closed, no content methods will be sent
                     chanClosed :: MVar (Maybe String),
                     consumers :: MVar (M.Map Text ((Message, Envelope) -> IO ())), -- who is consumer of a queue? (consumerTag => callback)
                     returnListeners :: MVar ([(Message, PublishError) -> IO ()]),
+                    confirmListeners :: MVar ([(Word64, Bool, AckType) -> IO ()]),
                     chanExceptionHandlers :: MVar [CE.SomeException -> IO ()]
                 }
 
@@ -516,6 +527,8 @@     isResponse (ContentMethod (Basic_return _ _ _ _) _ _) = False
     isResponse (SimpleMethod (Channel_flow _)) = False
     isResponse (SimpleMethod (Channel_close _ _ _ _)) = False
+    isResponse (SimpleMethod (Basic_ack _ _)) = False
+    isResponse (SimpleMethod (Basic_nack _ _ _)) = False
     isResponse _ = True
 
     --Basic.Deliver: forward msg to registered consumer
@@ -551,8 +564,31 @@         withMVar (returnListeners chan) $ \listeners ->
             forM_ listeners $ \l -> CE.catch (l (msg, pubError)) $ \(ex :: CE.SomeException) ->
                 hPutStrLn stderr $ "return listener on channel ["++(show $ channelID chan)++"] handling error ["++show pubError++"] threw exception: "++show ex
+    handleAsync (SimpleMethod (Basic_ack deliveryTag multiple)) = handleConfirm deliveryTag multiple BasicAck
+    handleAsync (SimpleMethod (Basic_nack deliveryTag multiple _)) = handleConfirm deliveryTag multiple BasicNack
     handleAsync m = error ("Unknown method: " ++ show m)
 
+    handleConfirm deliveryTag multiple k = do
+        withMVar (confirmListeners chan) $ \listeners ->
+            forM_ listeners $ \l -> CE.catch (l (deliveryTag, multiple, k)) $ \(ex :: CE.SomeException) ->
+                hPutStrLn stderr $ "confirm listener on channel ["++(show $ channelID chan)++"] handling method "++(show k)++" threw exception: "++ show ex
+
+        let seqNum = fromIntegral deliveryTag
+        let targetSet = case k of
+              BasicAck  -> (ackedSet chan)
+              BasicNack -> (nackedSet chan)
+        atomically $ do
+          unconfSet <- readTVar (unconfirmedSet chan)
+          let (merge, pending) = if multiple
+                                     then (IntSet.union confs, pending')
+                                     else (IntSet.insert seqNum, IntSet.delete seqNum unconfSet)
+                                  where
+                                    confs = fst parts
+                                    pending' = snd parts
+                                    parts = IntSet.partition (\n -> n <= seqNum) unconfSet
+          modifyTVar' targetSet (\ts -> merge ts)
+          writeTVar (unconfirmedSet chan) pending
+
     basicReturnToPublishError (Basic_return code (ShortString errText) (ShortString exchange) (ShortString routingKey)) =
         let replyError = case code of
                 312 -> Unroutable errText
@@ -604,13 +640,18 @@     ca <- newLock
     closed <- newMVar Nothing
     conss <- newMVar M.empty
-    listeners <- newMVar []
+    retListeners <- newMVar []
+    aSet <- newTVarIO IntSet.empty
+    nSet <- newTVarIO IntSet.empty
+    nxtSeq <- newIORef 0
+    unconfSet <- newTVarIO IntSet.empty
+    cnfListeners <- newMVar []
     handlers <- newMVar []
 
     --add new channel to connection's channel map
     newChannel <- modifyMVar (connChannels c) $ \mp -> do
         newChannelID <- allocateChannel (connChanAllocator c)
-        let newChannel = Channel c newInQueue outRes (fromIntegral newChannelID) lastConsTag ca closed conss listeners handlers
+        let newChannel = Channel c newInQueue outRes (fromIntegral newChannelID) lastConsTag nxtSeq unconfSet aSet nSet ca closed conss retListeners cnfListeners handlers
         thrID <- forkFinally' (channelReceiver newChannel) $ \res -> do
                    closeChannel' newChannel "closed"
                    case res of
@@ -719,3 +760,11 @@             case chc of
                 Just r -> CE.throwIO $ ChannelClosedException r
                 Nothing -> CE.throwIO $ ConnectionClosedException "unknown reason"
+
+waitForAllConfirms :: Channel -> STM (IntSet.IntSet, IntSet.IntSet)
+waitForAllConfirms chan = do
+  pending <- readTVar $ (unconfirmedSet chan)
+  check (IntSet.null pending)
+  return =<< (,)
+    <$> swapTVar (ackedSet chan) IntSet.empty
+    <*> swapTVar (nackedSet chan) IntSet.empty
Network/AMQP/Types.hs view
@@ -17,11 +17,13 @@     FieldTable(..),
     FieldValue(..),
     Decimals,
-    DecimalValue(..)
+    DecimalValue(..),
+    ConfirmationResult(..),
 ) where
 
 import Control.Applicative
 import Data.Int
+import Data.IntSet (IntSet)
 import Data.Binary
 import Data.Binary.Get
 import Data.Binary.IEEE754
@@ -209,3 +211,5 @@     put (DecimalValue a b) = put a >> put b
 
 type Decimals = Octet
+
+data ConfirmationResult = Complete (IntSet, IntSet) | Partial (IntSet, IntSet, IntSet) deriving Show
amqp.cabal view
@@ -1,5 +1,5 @@ Name:                amqp
-Version:             0.12.3
+Version:             0.13.0
 Synopsis:            Client library for AMQP servers (currently only RabbitMQ)
 Description:         Client library for AMQP servers (currently only RabbitMQ)
                      .
@@ -33,7 +33,8 @@     clock >= 0.4.0.1,
     monad-control >= 0.3,
     connection == 0.2.*,
-    vector
+    vector,
+    stm >= 2.4.0
   if flag(network-uri)
      build-depends: network-uri >= 2.6, network > 2.6
   else
@@ -85,7 +86,8 @@     , hspec-expectations >= 0.3.3
     , connection == 0.2.*
     , vector
+    , stm >= 2.4.0
   if flag(network-uri)
      build-depends: network-uri >= 2.6, network > 2.6
   else
-     build-depends: network < 2.6+     build-depends: network < 2.6