amqp 0.14.1 → 0.24.0
raw patch · 21 files changed
Files
- Network/AMQP.hs +199/−124
- Network/AMQP/ChannelAllocator.hs +1/−1
- Network/AMQP/Generated.hs +11/−1
- Network/AMQP/Helpers.hs +2/−2
- Network/AMQP/Internal.hs +350/−232
- Network/AMQP/Lifted.hs +5/−7
- Network/AMQP/Protocol.hs +13/−12
- Network/AMQP/Types.hs +23/−15
- amqp.cabal +16/−17
- changelog.md +73/−0
- examples/ExampleConsumer.hs +2/−0
- examples/ExampleProducer.hs +2/−0
- test/BasicPublishSpec.hs +67/−0
- test/ConnectionSpec.hs +1/−1
- test/ExchangeDeclareSpec.hs +4/−1
- test/ExchangeDeleteSpec.hs +7/−2
- test/FromURISpec.hs +190/−0
- test/QueueDeclareSpec.hs +5/−2
- test/QueueDeleteSpec.hs +6/−2
- test/QueuePurgeSpec.hs +3/−1
- test/Runner.hs +3/−0
Network/AMQP.hs view
@@ -1,11 +1,11 @@-{-# LANGUAGE BangPatterns, DeriveDataTypeable, OverloadedStrings, ScopedTypeVariables #-} +{-# LANGUAGE BangPatterns, DeriveDataTypeable, OverloadedStrings, ScopedTypeVariables, TupleSections #-} -- | -- -- A client library for AMQP servers implementing the 0-9-1 spec; currently only supports RabbitMQ (see <http://www.rabbitmq.com>) -- -- A good introduction to RabbitMQ and AMQP 0-9-1 (in various languages): <http://www.rabbitmq.com/getstarted.html>, <http://www.rabbitmq.com/tutorials/amqp-concepts.html> -- --- /Example/: +-- == Example -- -- Connect to a server, declare a queue and an exchange and setup a callback for messages coming in on the queue. Then publish a single message to our new exchange -- @@ -41,10 +41,18 @@ -- > -- acknowledge receiving the message -- > ackEnv env -- --- /Exception handling/: +-- == Exception handling notes -- -- Some function calls can make the AMQP server throw an AMQP exception, which has the side-effect of closing the connection or channel. The AMQP exceptions are raised as Haskell exceptions (see 'AMQPException'). So upon receiving an 'AMQPException' you may have to reopen the channel or connection. - +-- +-- == Debugging tips +-- +-- If you need to debug a problem with e.g. channels being closed unexpectedly, here are some tips: +-- +-- - The RabbitMQ log file often has helpful error-messages. The location of the log-file differs by OS. Look for RABBITMQ_LOGS in this page: <https://www.rabbitmq.com/relocate.html> +-- - The function 'addChannelExceptionHandler' can be used to figure out when and why a channel was closed. +-- - RabbitMQ has a browser-based management console, which allows you to see connections, channels, queues and more. Setup instructions are here: https://www.rabbitmq.com/management.html +-- module Network.AMQP ( -- * Connection Connection, @@ -57,12 +65,15 @@ closeChannel, closeConnection, addConnectionClosedHandler, + addConnectionBlockedHandler, + getServerProperties, -- * Channel Channel, openChannel, addReturnListener, addChannelExceptionHandler, + isNormalChannelClose, qos, -- * Exchanges @@ -107,6 +118,8 @@ ackMsg, ackEnv, + nackMsg, + nackEnv, -- * Transactions txSelect, @@ -119,6 +132,7 @@ waitForConfirmsUntil, addConfirmationListener, ConfirmationResult(..), + AckType(..), -- * Flow Control flow, @@ -131,6 +145,8 @@ -- * Exceptions AMQPException(..), + ChanThreadKilledException, + CloseType(..), -- * URI parsing fromURI @@ -143,7 +159,7 @@ import Data.List.Split (splitOn) import Data.Binary import Data.Binary.Put -import Network +import Network.Socket (PortNumber) import Network.URI (unEscapeString) import Data.Text (Text) @@ -158,23 +174,22 @@ import Network.AMQP.Generated import Network.AMQP.Internal import Network.AMQP.Helpers +import Text.Read (readMaybe) ----- EXCHANGE ----- -- | A record that contains the fields needed when creating a new exhange using 'declareExchange'. The default values apply when you use 'newExchange'. -data ExchangeOpts = ExchangeOpts - { - exchangeName :: Text, -- ^ (must be set); the name of the exchange - exchangeType :: Text, -- ^ (must be set); the type of the exchange (\"fanout\", \"direct\", \"topic\", \"headers\") +data ExchangeOpts = ExchangeOpts { + exchangeName :: Text, -- ^ (must be set); the name of the exchange + exchangeType :: Text, -- ^ (must be set); the type of the exchange (\"fanout\", \"direct\", \"topic\", \"headers\") - -- optional - exchangePassive :: Bool, -- ^ (default 'False'); If set, the server will not create the exchange. The client can use this to check whether an exchange exists without modifying the server state. - exchangeDurable :: Bool, -- ^ (default 'True'); If set when creating a new exchange, the exchange will be marked as durable. Durable exchanges remain active when a server restarts. Non-durable exchanges (transient exchanges) are purged if/when a server restarts. - exchangeAutoDelete :: Bool, -- ^ (default 'False'); If set, the exchange is deleted when all queues have finished using it. - exchangeInternal :: Bool, -- ^ (default 'False'); If set, the exchange may not be used directly by publishers, but only when bound to other exchanges. Internal exchanges are used to construct wiring that is not visible to applications. - exchangeArguments :: FieldTable -- ^ (default empty); A set of arguments for the declaration. The syntax and semantics of these arguments depends on the server implementation. - } - deriving (Eq, Ord, Read, Show) + -- optional + exchangePassive :: Bool, -- ^ (default 'False'); If set, the server will not create the exchange. The client can use this to check whether an exchange exists without modifying the server state. + exchangeDurable :: Bool, -- ^ (default 'True'); If set when creating a new exchange, the exchange will be marked as durable. Durable exchanges remain active when a server restarts. Non-durable exchanges (transient exchanges) are purged if/when a server restarts. + exchangeAutoDelete :: Bool, -- ^ (default 'False'); If set, the exchange is deleted when all queues have finished using it. + exchangeInternal :: Bool, -- ^ (default 'False'); If set, the exchange may not be used directly by publishers, but only when bound to other exchanges. Internal exchanges are used to construct wiring that is not visible to applications. + exchangeArguments :: FieldTable -- ^ (default empty); A set of arguments for the declaration. The syntax and semantics of these arguments depends on the server implementation. +} deriving (Eq, Ord, Read, Show) -- | an 'ExchangeOpts' with defaults set; you must override at least the 'exchangeName' and 'exchangeType' fields. newExchange :: ExchangeOpts @@ -244,19 +259,17 @@ ----- QUEUE ----- -- | A record that contains the fields needed when creating a new queue using 'declareQueue'. The default values apply when you use 'newQueue'. -data QueueOpts = QueueOpts - { - --must be set - queueName :: Text, -- ^ (default \"\"); the name of the queue; if left empty, the server will generate a new name and return it from the 'declareQueue' method +data QueueOpts = QueueOpts { + --must be set + queueName :: Text, -- ^ (default \"\"); the name of the queue; if left empty, the server will generate a new name and return it from the 'declareQueue' method - --optional - queuePassive :: Bool, -- ^ (default 'False'); If set, the server will not create the queue. The client can use this to check whether a queue exists without modifying the server state. - queueDurable :: Bool, -- ^ (default 'True'); If set when creating a new queue, the queue will be marked as durable. Durable queues remain active when a server restarts. Non-durable queues (transient queues) are purged if/when a server restarts. Note that durable queues do not necessarily hold persistent messages, although it does not make sense to send persistent messages to a transient queue. - queueExclusive :: Bool, -- ^ (default 'False'); Exclusive queues may only be consumed from by the current connection. Setting the 'exclusive' flag always implies 'auto-delete'. - queueAutoDelete :: Bool, -- ^ (default 'False'); If set, the queue is deleted when all consumers have finished using it. Last consumer can be cancelled either explicitly or because its channel is closed. If there was no consumer ever on the queue, it won't be deleted. - queueHeaders :: FieldTable -- ^ (default empty): Headers to use when creating this queue, such as @x-message-ttl@ or @x-dead-letter-exchange@. - } - deriving (Eq, Ord, Read, Show) + --optional + queuePassive :: Bool, -- ^ (default 'False'); If set, the server will not create the queue. The client can use this to check whether a queue exists without modifying the server state. + queueDurable :: Bool, -- ^ (default 'True'); If set when creating a new queue, the queue will be marked as durable. Durable queues remain active when a server restarts. Non-durable queues (transient queues) are purged if/when a server restarts. Note that durable queues do not necessarily hold persistent messages, although it does not make sense to send persistent messages to a transient queue. + queueExclusive :: Bool, -- ^ (default 'False'); Exclusive queues may only be consumed from by the current connection. Setting the 'exclusive' flag always implies 'auto-delete'. + queueAutoDelete :: Bool, -- ^ (default 'False'); If set, the queue is deleted when all consumers have finished using it. Last consumer can be cancelled either explicitly or because its channel is closed. If there was no consumer ever on the queue, it won't be deleted. + queueHeaders :: FieldTable -- ^ (default empty): Headers to use when creating this queue, such as @x-message-ttl@ or @x-dead-letter-exchange@. +} deriving (Eq, Ord, Read, Show) -- | a 'QueueOpts' with defaults set; you should override at least 'queueName'. newQueue :: QueueOpts @@ -269,15 +282,15 @@ -- @messageCount@ is the number of messages in the queue, which will be zero for newly-created queues. @consumerCount@ is the number of active consumers for the queue. declareQueue :: Channel -> QueueOpts -> IO (Text, Int, Int) declareQueue chan queue = do - (SimpleMethod (Queue_declare_ok (ShortString qName) messageCount consumerCount)) <- request chan $ (SimpleMethod (Queue_declare - 1 -- ticket - (ShortString $ queueName queue) - (queuePassive queue) - (queueDurable queue) - (queueExclusive queue) - (queueAutoDelete queue) - False -- no-wait; true means no answer from server - (queueHeaders queue))) + SimpleMethod (Queue_declare_ok (ShortString qName) messageCount consumerCount) <- request chan $ SimpleMethod $ Queue_declare + 1 -- ticket + (ShortString $ queueName queue) + (queuePassive queue) + (queueDurable queue) + (queueExclusive queue) + (queueAutoDelete queue) + False -- no-wait; true means no answer from server + (queueHeaders queue) return (qName, fromIntegral messageCount, fromIntegral consumerCount) -- | @bindQueue chan queue exchange routingKey@ binds the queue to the exchange using the provided routing key. If @exchange@ is the empty string, the default exchange will be used. @@ -316,32 +329,28 @@ -- | remove all messages from the queue; returns the number of messages that were in the queue purgeQueue :: Channel -> Text -> IO Word32 purgeQueue chan queue = do - (SimpleMethod (Queue_purge_ok msgCount)) <- request chan $ (SimpleMethod (Queue_purge + SimpleMethod (Queue_purge_ok msgCount) <- request chan $ SimpleMethod $ Queue_purge 1 -- ticket (ShortString queue) -- queue False -- nowait - )) return msgCount -- | deletes the queue; returns the number of messages that were in the queue before deletion deleteQueue :: Channel -> Text -> IO Word32 deleteQueue chan queue = do - (SimpleMethod (Queue_delete_ok msgCount)) <- request chan $ (SimpleMethod (Queue_delete + SimpleMethod (Queue_delete_ok msgCount) <- request chan $ SimpleMethod $ Queue_delete 1 -- ticket (ShortString queue) -- queue False -- if_unused False -- if_empty False -- nowait - )) return msgCount ----- MSG (the BASIC class in AMQP) ----- -- | a 'Msg' with defaults set; you should override at least 'msgBody' newMsg :: Message -newMsg = Message (BL.empty) Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing - -type ConsumerTag = Text +newMsg = Message BL.empty Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing -- | specifies whether you have to acknowledge messages that you receive from 'consumeMsgs' or 'getMsg'. If you use 'Ack', you have to call 'ackMsg' or 'ackEnv' after you have processed a message, otherwise it might be delivered again in the future data Ack = Ack | NoAck @@ -353,20 +362,47 @@ -- | @consumeMsgs chan queue ack callback@ subscribes to the given queue and returns a consumerTag. For any incoming message, the callback will be run. If @ack == 'Ack'@ you will have to acknowledge all incoming messages (see 'ackMsg' and 'ackEnv') -- --- NOTE: The callback will be run on the same thread as the channel thread (every channel spawns its own thread to listen for incoming data) so DO NOT perform any request on @chan@ inside the callback (however, you CAN perform requests on other open channels inside the callback, though I wouldn't recommend it). --- Functions that can safely be called on @chan@ are 'ackMsg', 'ackEnv', 'rejectMsg', 'recoverMsgs'. If you want to perform anything more complex, it's a good idea to wrap it inside 'forkIO'. +-- === Exceptions in the callback +-- +-- If an exception occurs in @callback@, it will be caught and printed to @stderr@. But you should not depend on this behaviour (it might change in future versions of this library); +-- instead, it is /strongly/ recommended that you catch any exceptions that your callback may throw and handle them appropriately. +-- But make sure not to catch 'ChanThreadKilledException' (or re-throw it if you did catch it), since it is used internally by the library to close channels. +-- +-- So unless you are confident that your callback won't throw exceptions, you may want to structure your code like this: +-- +-- > consumeMsgs chan name Ack $ \(msg, env) -> (do ...) +-- > `CE.catches` +-- > [ +-- > -- rethrow this exception, since the AMPQ library uses it internally +-- > CE.Handler $ \(e::ChanThreadKilledException) -> CE.throwIO e, +-- > +-- > -- (optional) catch individual exceptions that your code may throw +-- > CE.Handler $ \(e::CE.IOException) -> ..., +-- > CE.Handler $ \(e::SomeOtherException) -> ..., +-- > +-- > -- catch all exceptions that weren't handled above +-- > CE.Handler $ \(e::CE.SomeException) -> ... +-- > ] +-- +-- In practice, it might be useful to encapsulate this exception-handling logic in a custom wrapper-function so that you can reuse it for every callback you pass to @consumeMsgs@. +-- +-- === Blocking requests in the callback +-- +-- The @callback@ will be run on the channel's receiver thread (which is responsible for handling all incoming messages on this channel, including responses to requests from the client) so DO NOT perform any blocking request on @chan@ inside the callback, as this would lead to a dead-lock. However, you CAN perform requests on other open channels inside the callback, though that would keep @chan@ blocked until the requests are done, so it is not recommended. +-- +-- Unless you're using AMQP flow control, the following functions can safely be called on @chan@: 'ackMsg', 'ackEnv', 'rejectMsg', 'publishMsg'. If you use flow-control or want to perform anything more complex, it's recommended that instead of using 'consumeMsgs' you use 'getMsg' to fetch messages in a loop (because then your message-handling code will not run in the channel's receiver thread, so there will be no problems when performing blocking requests). consumeMsgs :: Channel -> Text -> Ack -> ((Message,Envelope) -> IO ()) -> IO ConsumerTag consumeMsgs chan queue ack callback = - consumeMsgs' chan queue ack callback (FieldTable M.empty) + consumeMsgs' chan queue ack callback (\_ -> return ()) (FieldTable M.empty) --- | an extended version of @consumeMsgs@ that allows you to include arbitrary arguments. -consumeMsgs' :: Channel -> Text -> Ack -> ((Message,Envelope) -> IO ()) -> FieldTable -> IO ConsumerTag -consumeMsgs' chan queue ack callback args = do +-- | an extended version of @consumeMsgs@ that allows you to define a consumer cancellation callback and include arbitrary arguments. +consumeMsgs' :: Channel -> Text -> Ack -> ((Message,Envelope) -> IO ()) -> (ConsumerTag -> IO ()) -> FieldTable -> IO ConsumerTag +consumeMsgs' chan queue ack callback cancelCB args = do --generate a new consumer tag - newConsumerTag <- (fmap (T.pack . show)) $ modifyMVar (lastConsumerTag chan) $ \c -> return (c+1,c+1) + newConsumerTag <- fmap (T.pack . show) $ modifyMVar (lastConsumerTag chan) $ \c -> return (c+1,c+1) --register the consumer - modifyMVar_ (consumers chan) $ return . M.insert newConsumerTag callback + modifyMVar_ (consumers chan) $ return . M.insert newConsumerTag (callback, cancelCB) writeAssembly chan (SimpleMethod $ Basic_consume 1 -- ticket @@ -383,10 +419,9 @@ -- | stops a consumer that was started with 'consumeMsgs' cancelConsumer :: Channel -> ConsumerTag -> IO () cancelConsumer chan consumerTag = do - (SimpleMethod (Basic_cancel_ok _)) <- request chan $ (SimpleMethod (Basic_cancel + SimpleMethod (Basic_cancel_ok _) <- request chan $ SimpleMethod $ Basic_cancel (ShortString consumerTag) -- consumer_tag False -- nowait - )) --unregister the consumer modifyMVar_ (consumers chan) $ return . M.delete consumerTag @@ -403,30 +438,31 @@ publishMsg' :: Channel -> Text -> Text -> Bool -> Message -> IO (Maybe Int) publishMsg' chan exchange routingKey mandatory msg = do modifyMVar (nextPublishSeqNum chan) $ \nxtSeqNum -> do - writeAssembly chan (ContentMethod (Basic_publish - 1 -- ticket; ignored by rabbitMQ - (ShortString exchange) - (ShortString routingKey) - mandatory -- mandatory; if true, the server might return the msg, which is currently not handled - False) --immediate; not customizable, as it is currently not supported anymore by RabbitMQ - + writeAssembly chan $ ContentMethod + (Basic_publish + 1 -- ticket; ignored by rabbitMQ + (ShortString exchange) + (ShortString routingKey) + mandatory -- mandatory; if true, the server might return the msg, which is currently not handled + False --immediate; not customizable, as it is currently not supported anymore by RabbitMQ + ) (CHBasic - (fmap ShortString $ msgContentType msg) - (fmap ShortString $ msgContentEncoding msg) - (msgHeaders msg) - (fmap deliveryModeToInt $ msgDeliveryMode msg) - (msgPriority msg) - (fmap ShortString $ msgCorrelationID msg) - (fmap ShortString $ msgReplyTo msg) - (fmap ShortString $ msgExpiration msg) - (fmap ShortString $ msgID msg) - (msgTimestamp msg) - (fmap ShortString $ msgType msg) - (fmap ShortString $ msgUserID msg) - (fmap ShortString $ msgApplicationID msg) - (fmap ShortString $ msgClusterID msg) + (ShortString <$> msgContentType msg) + (ShortString <$> msgContentEncoding msg) + (msgHeaders msg) + (deliveryModeToInt <$> msgDeliveryMode msg) + (msgPriority msg) + (ShortString <$> msgCorrelationID msg) + (ShortString <$> msgReplyTo msg) + (ShortString <$> msgExpiration msg) + (ShortString <$> msgID msg) + (msgTimestamp msg) + (ShortString <$> msgType msg) + (ShortString <$> msgUserID msg) + (ShortString <$> msgApplicationID msg) + (ShortString <$> msgClusterID msg) ) - (msgBody msg)) + (msgBody msg) if nxtSeqNum /= 0 then do @@ -434,19 +470,20 @@ return (succ nxtSeqNum, Just nxtSeqNum) else return (0, Nothing) --- | @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 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'). +-- +-- Will return @Nothing@ when no message is currently available. getMsg :: Channel -> Ack -> Text -> IO (Maybe (Message, Envelope)) getMsg chan ack queue = do - ret <- request chan (SimpleMethod (Basic_get + ret <- request chan $ SimpleMethod $ Basic_get 1 -- ticket (ShortString queue) -- queue (ackToBool ack) -- no_ack - )) case ret of ContentMethod (Basic_get_ok deliveryTag redelivered (ShortString exchange) (ShortString routingKey) _) properties body -> - return $ Just $ (msgFromContentHeaderProperties properties body, - Envelope {envDeliveryTag = deliveryTag, envRedelivered = redelivered, - envExchangeName = exchange, envRoutingKey = routingKey, envChannel = chan}) + return $ Just (msgFromContentHeaderProperties properties body, + Envelope {envDeliveryTag = deliveryTag, envRedelivered = redelivered, + envExchangeName = exchange, envRoutingKey = routingKey, envChannel = chan}) _ -> return Nothing {- | @ackMsg chan deliveryTag multiple@ acknowledges one or more messages. A message MUST not be acknowledged more than once. @@ -457,24 +494,39 @@ -} ackMsg :: Channel -> LongLongInt -> Bool -> IO () ackMsg chan deliveryTag multiple = - writeAssembly chan $ (SimpleMethod (Basic_ack + writeAssembly chan $ SimpleMethod $ Basic_ack deliveryTag -- delivery_tag multiple -- multiple - )) -- | Acknowledges a single message. This is a wrapper for 'ackMsg' in case you have the 'Envelope' at hand. ackEnv :: Envelope -> IO () ackEnv env = ackMsg (envChannel env) (envDeliveryTag env) False +{- | @nackMsg chan deliveryTag multiple requeue@ rejects one or more messages. A message MUST not be rejected more than once. + +if @multiple==True@, the @deliverTag@ is treated as \"up to and including\", so that the client can reject multiple messages with a single method call. If @multiple==False@, @deliveryTag@ refers to a single message. + +If @requeue==True@, the server will try to requeue the message. If @requeue==False@, the message will be dropped by the server. +-} +nackMsg :: Channel -> LongLongInt -> Bool -> Bool -> IO () +nackMsg chan deliveryTag multiple requeue = + writeAssembly chan $ SimpleMethod $ Basic_nack + deliveryTag -- delivery_tag + multiple -- multiple + requeue -- requeue + +-- | Reject a single message. This is a wrapper for 'nackMsg' in case you have the 'Envelope' at hand. +nackEnv :: Envelope -> IO () +nackEnv env = nackMsg (envChannel env) (envDeliveryTag env) False False + -- | @rejectMsg chan deliveryTag requeue@ allows a client to reject a message. It can be used to interrupt and cancel large incoming messages, or return untreatable messages to their original queue. If @requeue==False@, the message will be discarded. If it is 'True', the server will attempt to requeue the message. -- -- NOTE: RabbitMQ 1.7 doesn't implement this command rejectMsg :: Channel -> LongLongInt -> Bool -> IO () rejectMsg chan deliveryTag requeue = - writeAssembly chan $ (SimpleMethod (Basic_reject + writeAssembly chan $ SimpleMethod $ Basic_reject deliveryTag -- delivery_tag requeue -- requeue - )) -- | Reject a message. This is a wrapper for 'rejectMsg' in case you have the 'Envelope' at hand. rejectEnv :: Envelope @@ -486,9 +538,8 @@ --If @requeue==False@, the message will be redelivered to the original recipient. If @requeue==True@, the server will attempt to requeue the message, potentially then delivering it to an alternative subscriber. recoverMsgs :: Channel -> Bool -> IO () recoverMsgs chan requeue = do - SimpleMethod Basic_recover_ok <- request chan $ (SimpleMethod (Basic_recover + SimpleMethod Basic_recover_ok <- request chan $ SimpleMethod $ Basic_recover requeue -- requeue - )) return () ------------------- TRANSACTIONS (TX) -------------------------- @@ -496,19 +547,19 @@ -- | This method sets the channel to use standard transactions. The client must use this method at least once on a channel before using the Commit or Rollback methods. txSelect :: Channel -> IO () txSelect chan = do - (SimpleMethod Tx_select_ok) <- request chan $ SimpleMethod Tx_select + SimpleMethod Tx_select_ok <- request chan $ SimpleMethod Tx_select return () -- | This method commits all messages published and acknowledged in the current transaction. A new transaction starts immediately after a commit. txCommit :: Channel -> IO () txCommit chan = do - (SimpleMethod Tx_commit_ok) <- request chan $ SimpleMethod Tx_commit + SimpleMethod Tx_commit_ok <- request chan $ SimpleMethod Tx_commit return () -- | This method abandons all messages published and acknowledged in the current transaction. A new transaction starts immediately after a rollback. txRollback :: Channel -> IO () txRollback chan = do - (SimpleMethod Tx_rollback_ok) <- request chan $ SimpleMethod Tx_rollback + SimpleMethod Tx_rollback_ok <- request chan $ SimpleMethod Tx_rollback return () @@ -545,17 +596,17 @@ -} 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 + delay <- registerDelay timeout + let partial = do + expired <- readTVar delay + if expired + then Partial <$> ((,,) + <$> swapTVar (ackedSet chan) IntSet.empty + <*> swapTVar (nackedSet chan) IntSet.empty + <*> readTVar (unconfirmedSet chan)) + else retry + complete = 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 @@ -581,7 +632,7 @@ -} flow :: Channel -> Bool -> IO () flow chan active = do - (SimpleMethod (Channel_flow_ok _)) <- request chan $ SimpleMethod (Channel_flow active) + SimpleMethod (Channel_flow_ok _) <- request chan $ SimpleMethod (Channel_flow active) return () -- | Constructs default connection options with the following settings : @@ -595,7 +646,7 @@ -- * no limit on the number of used channels -- defaultConnectionOpts :: ConnectionOpts -defaultConnectionOpts = ConnectionOpts [("localhost", 5672)] "/" [plain "guest" "guest"] (Just 131072) Nothing Nothing Nothing +defaultConnectionOpts = ConnectionOpts [("localhost", 5672)] "/" [plain "guest" "guest"] (Just 131072) Nothing Nothing Nothing Nothing -- | @openConnection hostname virtualHost loginName loginPassword@ opens a connection to an AMQP server running on @hostname@. -- @virtualHost@ is used as a namespace for AMQP resources (default is \"/\"), so different applications could use multiple virtual hosts on the same AMQP server. @@ -621,13 +672,13 @@ plain loginName loginPassword = SASLMechanism "PLAIN" initialResponse Nothing where nul = '\0' - initialResponse = E.encodeUtf8 $ (T.cons nul loginName) `T.append` (T.cons nul loginPassword) + initialResponse = E.encodeUtf8 $ T.cons nul loginName `T.append` T.cons nul loginPassword -- | The @AMQPLAIN@ SASL mechanism. See <http://www.rabbitmq.com/authentication.html>. amqplain :: Text -> Text -> SASLMechanism amqplain loginName loginPassword = SASLMechanism "AMQPLAIN" initialResponse Nothing where - initialResponse = toStrict $ BL.drop 4 $ runPut $ put $ FieldTable $ M.fromList [("LOGIN",FVString loginName), ("PASSWORD", FVString loginPassword)] + initialResponse = toStrict $ BL.drop 4 $ runPut $ put $ FieldTable $ M.fromList [("LOGIN",FVString $ E.encodeUtf8 loginName), ("PASSWORD", FVString $ E.encodeUtf8 loginPassword)] -- | The @RABBIT-CR-DEMO@ SASL mechanism needs to be explicitly enabled on the RabbitMQ server and should only be used for demonstration purposes of the challenge-response cycle. -- See <http://www.rabbitmq.com/authentication.html>. @@ -635,7 +686,7 @@ rabbitCRdemo loginName loginPassword = SASLMechanism "RABBIT-CR-DEMO" initialResponse (Just $ const challengeResponse) where initialResponse = E.encodeUtf8 loginName - challengeResponse = return $ (E.encodeUtf8 "My password is ") `BS.append` (E.encodeUtf8 loginPassword) + challengeResponse = return $ E.encodeUtf8 "My password is " `BS.append` E.encodeUtf8 loginPassword -- | @qos chan prefetchSize prefetchCount global@ limits the amount of data the server -- delivers before requiring acknowledgements. @prefetchSize@ specifies the @@ -647,33 +698,57 @@ -- NOTE: RabbitMQ does not implement prefetchSize and will throw an exception if it doesn't equal 0. qos :: Channel -> Word32 -> Word16 -> Bool -> IO () qos chan prefetchSize prefetchCount global = do - (SimpleMethod Basic_qos_ok) <- request chan (SimpleMethod (Basic_qos + SimpleMethod Basic_qos_ok <- request chan $ SimpleMethod $ Basic_qos prefetchSize prefetchCount global - )) return () --- | Parses amqp standard URI of the form @amqp://user:password@host:port/vhost@ and returns a @ConnectionOpts@ for use with @openConnection''@ --- | Any of these fields may be empty and will be replaced with defaults from @amqp://guest:guest@localhost:5672/@ -fromURI :: String -> ConnectionOpts -fromURI uri = defaultConnectionOpts { - coServers = [(host,fromIntegral nport)], - coVHost = (T.pack vhost), - coAuth = [plain (T.pack uid) (T.pack pw)] - } - where (host,nport,uid,pw,vhost) = fromURI' uri +-- | Parses an AMQP standard URI of the form @amqp://user:password\@host:port\/vhost@ and returns a 'ConnectionOpts' for use with 'openConnection'''. +-- +-- To pass multiple servers, separate them by comma, like: @amqp://user:password\@host:port,host2:port2\/vhost@ +-- +-- Any of these fields may be empty and will be replaced with defaults from @amqp://guest:guest\@localhost:5672\/@ +-- +-- When parsing fails, a @Left String@ will be returned with a human-readable error-message. +fromURI :: String -> Either String ConnectionOpts +fromURI uri = + case fromURI' uri of + Right (hostPorts, uid, pw, vhost, tls) -> + Right $ defaultConnectionOpts { + coServers = hostPorts, + coVHost = T.pack vhost, + coAuth = [plain (T.pack uid) (T.pack pw)], + coTLSSettings = if tls then Just TLSTrusted else Nothing + } + Left err -> Left err -fromURI' :: String -> (String,Int,String,String,String) -fromURI' uri = (unEscapeString host, nport, unEscapeString (dropWhile (=='/') uid), unEscapeString pw, unEscapeString vhost) +fromURI' :: String -> Either String ([(String, PortNumber)], String, String, String, Bool) +fromURI' uri = + case sequence (map (fromHostPort dport) hostPorts) of + Right hostPorts' -> Right ( + hostPorts', + unEscapeString (dropWhile (=='/') uid), + unEscapeString pw, + unEscapeString vhost, + tls + ) + Left err -> Left err where (pre :suf : _) = splitOn "@" (uri ++ "@" ) -- look mom, no regexp dependencies (pro :uid' :pw':_) = splitOn ":" (pre ++ "::") (hnp :thost: _) = splitOn "/" (suf ++ "/" ) - (hst':port : _) = splitOn ":" (hnp ++ ":" ) + hostPorts = splitOn "," hnp vhost = if null thost then "/" else thost dport = if pro == "amqps" then 5671 else 5672 - nport = if null port then dport else read port uid = if null uid' then "guest" else uid' pw = if null pw' then "guest" else pw' - host = if null hst' then "localhost" else hst' + tls = pro == "amqps" +fromHostPort :: PortNumber -> String -> Either String (String, PortNumber) +fromHostPort defPort hostPort = (unEscapeString host, ) <$> nport + where + (hst':port : _) = splitOn ":" (hostPort ++ ":" ) + host = if null hst' then "localhost" else hst' + nport = if null port + then Right defPort + else maybe (Left $ "invalid port number: "++port) Right $ readMaybe port
Network/AMQP/ChannelAllocator.hs view
@@ -14,7 +14,7 @@ newChannelAllocator :: Int -> IO ChannelAllocator newChannelAllocator maxChannel = - fmap (ChannelAllocator maxChannel) $ V.replicate 1024 0 + ChannelAllocator maxChannel <$> V.replicate 1024 0 allocateChannel :: ChannelAllocator -> IO Int allocateChannel (ChannelAllocator maxChannel c) = do
Network/AMQP/Generated.hs view
@@ -110,6 +110,8 @@ put (Connection_open_ok a) = putWord16be 10 >> putWord16be 41 >> put a put (Connection_close a b c d) = putWord16be 10 >> putWord16be 50 >> put a >> put b >> put c >> put d put Connection_close_ok = putWord16be 10 >> putWord16be 51 + put (Connection_blocked a) = putWord16be 10 >> putWord16be 60 >> put a + put Connection_unblocked = putWord16be 10 >> putWord16be 61 put (Channel_open a) = putWord16be 20 >> putWord16be 10 >> put a put (Channel_open_ok a) = putWord16be 20 >> putWord16be 11 >> put a put (Channel_flow a) = putWord16be 20 >> putWord16be 20 >> put a @@ -174,6 +176,8 @@ (10,41) -> get >>= \a -> return (Connection_open_ok a) (10,50) -> get >>= \a -> get >>= \b -> get >>= \c -> get >>= \d -> return (Connection_close a b c d) (10,51) -> return Connection_close_ok + (10,60) -> get >>= \a -> return (Connection_blocked a) + (10,61) -> return Connection_unblocked (20,10) -> get >>= \a -> return (Channel_open a) (20,11) -> get >>= \a -> return (Channel_open_ok a) (20,20) -> get >>= \a -> return (Channel_flow a) @@ -273,6 +277,12 @@ Connection_close_ok | + Connection_blocked + ShortString -- reason + | + Connection_unblocked + + | Channel_open ShortString -- reserved-1 | @@ -502,4 +512,4 @@ Confirm_select_ok - deriving Show+ deriving Show
Network/AMQP/Helpers.hs view
@@ -43,11 +43,11 @@ chooseMin a Nothing = a getTimestamp :: IO Int64 -getTimestamp = fmap µs $ getTime Monotonic +getTimestamp = µs <$> getTime Monotonic where seconds spec = (fromIntegral . sec) spec * 1000 * 1000 micros spec = (fromIntegral . nsec) spec `div` 1000 - µs spec = (seconds spec) + (micros spec) + µs spec = seconds spec + micros spec scheduleAtFixedRate :: Int -> IO () -> IO ThreadId scheduleAtFixedRate interval_µs action = forkIO $ forever $ do
Network/AMQP/Internal.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE BangPatterns, CPP, OverloadedStrings, ScopedTypeVariables #-} +{-# LANGUAGE BangPatterns, CPP, OverloadedStrings, ScopedTypeVariables, LambdaCase #-} module Network.AMQP.Internal where import Paths_amqp(version) @@ -11,10 +11,11 @@ import Data.Binary import Data.Binary.Get import Data.Binary.Put as BPut +import Data.Default.Class import Data.Int (Int64) import Data.Maybe import Data.Text (Text) -import Network +import Network.Socket (PortNumber, withSocketsDo) import System.IO (hPutStrLn, stderr) import qualified Control.Exception as CE @@ -53,41 +54,37 @@ -- | An AMQP message data Message = Message { - msgBody :: BL.ByteString, -- ^ the content of your message - msgDeliveryMode :: Maybe DeliveryMode, -- ^ see 'DeliveryMode' - msgTimestamp :: Maybe Timestamp, -- ^ use in any way you like; this doesn't affect the way the message is handled - msgID :: Maybe Text, -- ^ use in any way you like; this doesn't affect the way the message is handled - msgType :: Maybe Text, -- ^ use in any way you like; this doesn't affect the way the message is handled - msgUserID :: Maybe Text, - msgApplicationID :: Maybe Text, - msgClusterID :: Maybe Text, - msgContentType :: Maybe Text, - msgContentEncoding :: Maybe Text, - msgReplyTo :: Maybe Text, - msgPriority :: Maybe Octet, - msgCorrelationID :: Maybe Text, - msgExpiration :: Maybe Text, - msgHeaders :: Maybe FieldTable - } - deriving (Eq, Ord, Read, Show) + msgBody :: BL.ByteString, -- ^ the content of your message + msgDeliveryMode :: Maybe DeliveryMode, -- ^ see 'DeliveryMode' + msgTimestamp :: Maybe Timestamp, -- ^ use in any way you like; this doesn't affect the way the message is handled + msgID :: Maybe Text, -- ^ use in any way you like; this doesn't affect the way the message is handled + msgType :: Maybe Text, -- ^ use in any way you like; this doesn't affect the way the message is handled + msgUserID :: Maybe Text, + msgApplicationID :: Maybe Text, + msgClusterID :: Maybe Text, + msgContentType :: Maybe Text, + msgContentEncoding :: Maybe Text, + msgReplyTo :: Maybe Text, + msgPriority :: Maybe Octet, + msgCorrelationID :: Maybe Text, + msgExpiration :: Maybe Text, + msgHeaders :: Maybe FieldTable +} deriving (Eq, Ord, Read, Show) -- | contains meta-information of a delivered message (through 'getMsg' or 'consumeMsgs') -data Envelope = Envelope - { - envDeliveryTag :: LongLongInt, - envRedelivered :: Bool, - envExchangeName :: Text, - envRoutingKey :: Text, - envChannel :: Channel - } +data Envelope = Envelope { + envDeliveryTag :: LongLongInt, + envRedelivered :: Bool, + envExchangeName :: Text, + envRoutingKey :: Text, + envChannel :: Channel +} -data PublishError = PublishError - { - errReplyCode :: ReturnReplyCode, - errExchange :: Maybe Text, - errRoutingKey :: Text - } - deriving (Eq, Read, Show) +data PublishError = PublishError { + errReplyCode :: ReturnReplyCode, + errExchange :: Maybe Text, + errRoutingKey :: Text +} deriving (Eq, Read, Show) data ReturnReplyCode = Unroutable Text | NoConsumers Text @@ -126,7 +123,7 @@ collect x | x <= 0 = return [] collect x = do (ContentBodyPayload payload) <- readChan chan - r <- collect (x - (BL.length payload)) + r <- collect (x - BL.length payload) return $ payload : r ------------ CONNECTION ------------------- @@ -141,50 +138,58 @@ -} data Connection = Connection { - connHandle :: Conn.Connection, - connChanAllocator :: ChannelAllocator, - connChannels :: MVar (IM.IntMap (Channel, ThreadId)), -- open channels (channelID => (Channel, ChannelThread)) - connMaxFrameSize :: Int, --negotiated maximum frame size - connClosed :: MVar (Maybe String), - connClosedLock :: MVar (), -- used by closeConnection to block until connection-close handshake is complete - connWriteLock :: MVar (), -- to ensure atomic writes to the socket - connClosedHandlers :: MVar [IO ()], - connLastReceived :: MVar Int64, -- the timestamp from a monotonic clock when the last frame was received - connLastSent :: MVar Int64 -- the timestamp from a monotonic clock when the last frame was written - } + connHandle :: Conn.Connection, + connChanAllocator :: ChannelAllocator, + connChannels :: MVar (IM.IntMap (Channel, ThreadId)), -- open channels (channelID => (Channel, ChannelThread)) + connMaxFrameSize :: Int, --negotiated maximum frame size + connClosed :: MVar (Maybe (CloseType, String)), + connClosedLock :: MVar (), -- used by closeConnection to block until connection-close handshake is complete + connWriteLock :: MVar (), -- to ensure atomic writes to the socket + connClosedHandlers :: MVar [IO ()], + connBlockedHandlers :: MVar [(Text -> IO (), IO ())], + connLastReceived :: MVar Int64, -- the timestamp from a monotonic clock when the last frame was received + connLastSent :: MVar Int64, -- the timestamp from a monotonic clock when the last frame was written + connServerProperties :: FieldTable, -- the server properties sent in Connection_start + connThread :: MVar (Maybe ThreadId) +} -- | Represents the parameters to connect to a broker or a cluster of brokers. -- See 'defaultConnectionOpts'. data ConnectionOpts = ConnectionOpts { - coServers :: ![(String, PortNumber)], -- ^ A list of host-port pairs. Useful in a clustered setup to connect to the first available host. - coVHost :: !Text, -- ^ The VHost to connect to. - coAuth :: ![SASLMechanism], -- ^ The 'SASLMechanism's to use for authenticating with the broker. - coMaxFrameSize :: !(Maybe Word32), -- ^ The maximum frame size to be used. If not specified, no limit is assumed. - coHeartbeatDelay :: !(Maybe Word16), -- ^ The delay in seconds, after which the client expects a heartbeat frame from the broker. If 'Nothing', the value suggested by the broker is used. Use @Just 0@ to disable the heartbeat mechnism. - coMaxChannel :: !(Maybe Word16), -- ^ The maximum number of channels the client will use. - coTLSSettings :: Maybe TLSSettings -- ^ Whether or not to connect to servers using TLS. See http://www.rabbitmq.com/ssl.html for details. - } + coServers :: ![(String, PortNumber)], -- ^ A list of host-port pairs. Useful in a clustered setup to connect to the first available host. + coVHost :: !Text, -- ^ The VHost to connect to. + coAuth :: ![SASLMechanism], -- ^ The 'SASLMechanism's to use for authenticating with the broker. + coMaxFrameSize :: !(Maybe Word32), -- ^ The maximum frame size to be used. If not specified, no limit is assumed. + coHeartbeatDelay :: !(Maybe Word16), -- ^ The delay in seconds, after which the client expects a heartbeat frame from the broker. If 'Nothing', the value suggested by the broker is used. Use @Just 0@ to disable the heartbeat mechnism. + coMaxChannel :: !(Maybe Word16), -- ^ The maximum number of channels the client will use. + coTLSSettings :: Maybe TLSSettings, -- ^ Whether or not to connect to servers using TLS. See http://www.rabbitmq.com/ssl.html for details. + coName :: !(Maybe Text) -- ^ optional connection name (will be displayed in the RabbitMQ web interface) +} deriving Show + -- | Represents the kind of TLS connection to establish. data TLSSettings = TLSTrusted -- ^ Require trusted certificates (Recommended). | TLSUntrusted -- ^ Allow untrusted certificates (Discouraged. Vulnerable to man-in-the-middle attacks) | TLSCustom Conn.TLSSettings -- ^ Provide your own custom TLS settings - + deriving Show connectionTLSSettings :: TLSSettings -> Maybe Conn.TLSSettings connectionTLSSettings tlsSettings = - Just $ case tlsSettings of - TLSTrusted -> Conn.TLSSettingsSimple False False False - TLSUntrusted -> Conn.TLSSettingsSimple True False False - TLSCustom x -> x + Just $ case tlsSettings of + TLSTrusted -> Conn.TLSSettingsSimple False False False def + TLSUntrusted -> Conn.TLSSettingsSimple True False False def + TLSCustom x -> x -- | A 'SASLMechanism' is described by its name ('saslName'), its initial response ('saslInitialResponse'), and an optional function ('saslChallengeFunc') that -- transforms a security challenge provided by the server into response, which is then sent back to the server for verification. data SASLMechanism = SASLMechanism { - saslName :: !Text, -- ^ mechanism name - saslInitialResponse :: !BS.ByteString, -- ^ initial response - saslChallengeFunc :: !(Maybe (BS.ByteString -> IO BS.ByteString)) -- ^ challenge processing function - } + saslName :: !Text, -- ^ mechanism name + saslInitialResponse :: !BS.ByteString, -- ^ initial response + saslChallengeFunc :: !(Maybe (BS.ByteString -> IO BS.ByteString)) -- ^ challenge processing function +} +instance Show SASLMechanism where + show x = "SASL" + -- | reads incoming frames from socket and forwards them to the opened channels connectionReceiver :: Connection -> IO () connectionReceiver conn = do @@ -193,40 +198,54 @@ updateLastReceived conn forwardToChannel chanID payload ) - (\(e :: CE.IOException) -> myThreadId >>= killConnection conn (CE.toException e)) + (\(e :: CE.IOException) -> myThreadId >>= killConnection conn Abnormal (CE.toException e)) connectionReceiver conn where - - closedByUserEx = ConnectionClosedException "closed by user" + closedByUserEx = ConnectionClosedException Normal "closed by user" - forwardToChannel 0 (MethodPayload Connection_close_ok) = myThreadId >>= killConnection conn (CE.toException closedByUserEx) + forwardToChannel 0 (MethodPayload Connection_close_ok) = + myThreadId >>= killConnection conn Normal (CE.toException closedByUserEx) forwardToChannel 0 (MethodPayload (Connection_close _ (ShortString errorMsg) _ _)) = do writeFrame (connHandle conn) $ Frame 0 $ MethodPayload Connection_close_ok - myThreadId >>= killConnection conn (CE.toException . ConnectionClosedException . T.unpack $ errorMsg) + myThreadId >>= killConnection conn Abnormal (CE.toException . ConnectionClosedException Abnormal . T.unpack $ errorMsg) forwardToChannel 0 HeartbeatPayload = return () + forwardToChannel 0 (MethodPayload (Connection_blocked reason)) = handleBlocked reason + forwardToChannel 0 (MethodPayload Connection_unblocked) = handleUnblocked forwardToChannel 0 payload = hPutStrLn stderr $ "Got unexpected msg on channel zero: " ++ show payload forwardToChannel chanID payload = do --got asynchronous msg => forward to registered channel withMVar (connChannels conn) $ \cs -> do case IM.lookup (fromIntegral chanID) cs of Just c -> writeChan (inQueue $ fst c) payload + -- TODO: It's unclear how to handle this, although it probably never + -- happens in practice. Nothing -> hPutStrLn stderr $ "ERROR: channel not open " ++ show chanID + handleBlocked (ShortString reason) = do + withMVar (connBlockedHandlers conn) $ \listeners -> + forM_ listeners $ \(l, _) -> CE.catch (l reason) $ \(ex :: CE.SomeException) -> + hPutStrLn stderr $ "connection blocked listener threw exception: "++ show ex + + handleUnblocked = do + withMVar (connBlockedHandlers conn) $ \listeners -> + forM_ listeners $ \(_, l) -> CE.catch l $ \(ex :: CE.SomeException) -> + hPutStrLn stderr $ "connection unblocked listener threw exception: "++ show ex + -- | Opens a connection to a broker specified by the given 'ConnectionOpts' parameter. openConnection'' :: ConnectionOpts -> IO Connection openConnection'' connOpts = withSocketsDo $ do handle <- connect [] $ coServers connOpts - (maxFrameSize, maxChannel, heartbeatTimeout) <- CE.handle (\(_ :: CE.IOException) -> CE.throwIO $ ConnectionClosedException "Handshake failed. Please check the RabbitMQ logs for more information") $ do + (maxFrameSize, maxChannel, heartbeatTimeout, serverProps) <- CE.handle (\(_ :: CE.IOException) -> CE.throwIO $ ConnectionClosedException Abnormal "Handshake failed. Please check the RabbitMQ logs for more information") $ do Conn.connectionPut handle $ BS.append (BC.pack "AMQP") - (BS.pack [ - 1 - , 1 --TCP/IP - , 0 --Major Version - , 9 --Minor Version - ]) + (BS.pack [ + 1 + , 1 --TCP/IP + , 0 --Major Version + , 9 --Minor Version + ]) -- S: connection.start - Frame 0 (MethodPayload (Connection_start _ _ _ (LongString serverMechanisms) _)) <- readFrame handle + Frame 0 (MethodPayload (Connection_start _ _ serverProps (LongString serverMechanisms) _)) <- readFrame handle selectedSASL <- selectSASLMechanism handle serverMechanisms -- C: start_ok @@ -251,7 +270,7 @@ Frame 0 (MethodPayload (Connection_open_ok _)) <- readFrame handle -- Connection established! - return (maxFrameSize, maxChannel, heartbeatTimeout) + return (maxFrameSize, maxChannel, heartbeatTimeout, serverProps) --build Connection object cChannels <- newMVar IM.empty @@ -261,58 +280,60 @@ writeLock <- newMVar () ccl <- newEmptyMVar cClosedHandlers <- newMVar [] + cBlockedHandlers <- newMVar [] cLastReceived <- getTimestamp >>= newMVar cLastSent <- getTimestamp >>= newMVar - let conn = Connection handle cChanAllocator cChannels (fromIntegral maxFrameSize) cClosed ccl writeLock cClosedHandlers cLastReceived cLastSent + cThread <- newMVar Nothing + let conn = Connection handle cChanAllocator cChannels (fromIntegral maxFrameSize) cClosed ccl writeLock cClosedHandlers cBlockedHandlers cLastReceived cLastSent serverProps cThread + --spawn the connectionReceiver - connThread <- forkFinally' (connectionReceiver conn) $ \res -> do - -- try closing socket - CE.catch (Conn.connectionClose handle) (\(_ :: CE.SomeException) -> return ()) + connThreadId <- forkFinally' (connectionReceiver conn) $ \res -> do + -- try closing socket + CE.catch (Conn.connectionClose handle) (\(_ :: CE.SomeException) -> return ()) - -- mark as closed - modifyMVar_ cClosed $ return . Just . maybe "unknown reason" id + -- mark as closed + modifyMVar_ cClosed $ return . Just . fromMaybe (Abnormal, "unknown reason") - -- kill all channel-threads, making sure the channel threads will - -- be killed taking into account the overall state of the - -- connection: if the thread died for an unexpected exception, - -- inform the channel threads downstream accordingly. Otherwise - -- just use a normal 'killThread' finaliser. - let finaliser = case res of - Left ex -> ex - Right _ -> CE.toException CE.ThreadKilled - modifyMVar_ cChannels $ \x -> do - mapM_ (flip CE.throwTo finaliser . snd) $ IM.elems x - return IM.empty + -- kill all channel-threads, making sure the channel threads will + -- be killed taking into account the overall state of the + -- connection: if the thread died for an unexpected exception, + -- inform the channel threads downstream accordingly. Otherwise + -- just use a normal 'killThread' finaliser. + let finaliser = ChanThreadKilledException $ case res of + Left ex -> ex + Right _ -> CE.toException CE.ThreadKilled + modifyMVar_ cChannels $ \x -> do + mapM_ (flip CE.throwTo finaliser . snd) $ IM.elems x + return IM.empty - -- mark connection as closed, so all pending calls to 'closeConnection' can now return - void $ tryPutMVar ccl () + -- mark connection as closed, so all pending calls to 'closeConnection' can now return + void $ tryPutMVar ccl () - -- notify connection-close-handlers - withMVar cClosedHandlers sequence_ + -- notify connection-close-handlers + withMVar cClosedHandlers sequence_ case heartbeatTimeout of Nothing -> return () - Just timeout -> do - heartbeatThread <- watchHeartbeats conn (fromIntegral timeout) connThread + Just timeout -> do + heartbeatThread <- watchHeartbeats conn (fromIntegral timeout) connThreadId addConnectionClosedHandler conn True (killThread heartbeatThread) + modifyMVar_ cThread $ \_ -> return $ Just connThreadId return conn where connect excs ((host, port) : rest) = do ctx <- Conn.initConnectionContext - result <- CE.try (Conn.connectTo ctx $ Conn.ConnectionParams - { Conn.connectionHostname = host - , Conn.connectionPort = port - , Conn.connectionUseSecure = tlsSettings - , Conn.connectionUseSocks = Nothing - }) - either - (\(ex :: CE.SomeException) -> do - connect (ex:excs) rest) - (return) - result - connect excs [] = CE.throwIO $ ConnectionClosedException $ "Could not connect to any of the provided brokers: " ++ show (zip (coServers connOpts) (reverse excs)) + result <- CE.try $ Conn.connectTo ctx $ Conn.ConnectionParams + { Conn.connectionHostname = host + , Conn.connectionPort = port + , Conn.connectionUseSecure = tlsSettings + , Conn.connectionUseSocks = Nothing + } + either (\(ex :: CE.SomeException) -> connect (ex:excs) rest) + return + result + connect excs [] = CE.throwIO $ ConnectionClosedException Abnormal $ "Could not connect to any of the provided brokers: " ++ show (zip (coServers connOpts) (reverse excs)) tlsSettings = maybe Nothing connectionTLSSettings (coTLSSettings connOpts) selectSASLMechanism handle serverMechanisms = let serverSaslList = T.split (== ' ') $ E.decodeUtf8 serverMechanisms @@ -322,13 +343,23 @@ in abortIfNothing maybeSasl handle ("None of the provided SASL mechanisms "++show clientSaslList++" is supported by the server "++show serverSaslList++".") - start_ok sasl = (Frame 0 (MethodPayload (Connection_start_ok clientProperties - (ShortString $ saslName sasl) - (LongString $ saslInitialResponse sasl) - (ShortString "en_US")) )) - where clientProperties = FieldTable $ M.fromList [ ("platform", FVString "Haskell") - , ("version" , FVString . T.pack $ showVersion version)] + start_ok sasl = Frame 0 $ MethodPayload $ Connection_start_ok + clientProperties + (ShortString $ saslName sasl) + (LongString $ saslInitialResponse sasl) + (ShortString "en_US") + where + clientProperties = FieldTable $ M.fromList $ [ + ("platform", FVString "Haskell"), + ("version" , FVString . E.encodeUtf8 . T.pack $ showVersion version), + ("capabilities", FVFieldTable clientCapabilities) + ] ++ maybe [] (\x -> [("connection_name", FVString $ E.encodeUtf8 x)]) (coName connOpts) + clientCapabilities = FieldTable $ M.fromList [ + ("consumer_cancel_notify", FVBool True), + ("connection.blocked", FVBool True) + ] + handleSecureUntilTune handle sasl = do tuneOrSecure <- readFrame handle case tuneOrSecure of @@ -339,17 +370,17 @@ writeFrame handle (Frame 0 (MethodPayload (Connection_secure_ok (LongString challengeResponse)))) handleSecureUntilTune handle sasl - tune@(Frame 0 (MethodPayload (Connection_tune _ _ _))) -> return tune + tune@(Frame 0 (MethodPayload Connection_tune{})) -> return tune x -> error $ "handleSecureUntilTune fail. received message: "++show x - open = (Frame 0 (MethodPayload (Connection_open + open = Frame 0 $ MethodPayload $ Connection_open (ShortString $ coVHost connOpts) (ShortString $ T.pack "") -- capabilities; deprecated in 0-9-1 - True))) -- insist; deprecated in 0-9-1 + True -- insist; deprecated in 0-9-1 abortHandshake handle msg = do Conn.connectionClose handle - CE.throwIO $ ConnectionClosedException msg + CE.throwIO $ ConnectionClosedException Abnormal msg abortIfNothing m handle msg = case m of Nothing -> abortHandshake handle msg @@ -362,21 +393,20 @@ checkReceiveTimeout where rate = timeout * 1000 * 250 -- timeout / 4 in µs - receiveTimeout = (fromIntegral rate) * 4 * 2 -- 2*timeout in µs - sendTimeout = (fromIntegral rate) * 2 -- timeout/2 in µs + receiveTimeout = fromIntegral rate * 4 * 2 -- 2*timeout in µs + sendTimeout = fromIntegral rate * 2 -- timeout/2 in µs - skippedBeatEx = ConnectionClosedException "killed connection after missing 2 consecutive heartbeats" + skippedBeatEx = ConnectionClosedException Abnormal "killed connection after missing 2 consecutive heartbeats" - checkReceiveTimeout = doCheck (connLastReceived conn) receiveTimeout - (killConnection conn (CE.toException skippedBeatEx) connThread) + checkReceiveTimeout = doCheck (connLastReceived conn) receiveTimeout $ + killConnection conn Abnormal (CE.toException skippedBeatEx) connThread - checkSendTimeout = doCheck (connLastSent conn) sendTimeout - (writeFrame (connHandle conn) (Frame 0 HeartbeatPayload)) + checkSendTimeout = doCheck (connLastSent conn) sendTimeout $ + writeFrame (connHandle conn) (Frame 0 HeartbeatPayload) doCheck var timeout_µs action = withMVar var $ \lastFrameTime -> do time <- getTimestamp - when (time >= lastFrameTime + timeout_µs) $ do - action + when (time >= lastFrameTime + timeout_µs) action updateLastSent :: Connection -> IO () updateLastSent conn = modifyMVar_ (connLastSent conn) (const getTimestamp) @@ -385,10 +415,10 @@ updateLastReceived conn = modifyMVar_ (connLastReceived conn) (const getTimestamp) -- | kill the connection thread abruptly -killConnection :: Connection -> CE.SomeException -> ThreadId -> IO () -killConnection conn ex connThread = do - modifyMVar_ (connClosed conn) $ const $ return $ Just (show ex) - throwTo connThread ex +killConnection :: Connection -> CloseType -> CE.SomeException -> ThreadId -> IO () +killConnection conn closeType ex connThreadId = do + modifyMVar_ (connClosed conn) $ const $ return $ Just (closeType, show ex) + throwTo connThreadId ex -- | closes a connection -- @@ -396,34 +426,47 @@ closeConnection :: Connection -> IO () closeConnection c = do CE.catch ( - withMVar (connWriteLock c) $ \_ -> writeFrame (connHandle c) $ (Frame 0 (MethodPayload (Connection_close + withMVar (connWriteLock c) $ \_ -> writeFrame (connHandle c) $ Frame 0 $ MethodPayload $ Connection_close --TODO: set these values 0 -- reply_code (ShortString "") -- reply_text 0 -- class_id 0 -- method_id - ))) - ) - (\ (_ :: CE.IOException) -> - --do nothing if connection is already closed - return () ) + (\ (e :: CE.IOException) -> do + -- Make sure the connection is closed. + -- (This pattern match can't fail, since openConnection'' will always fill + -- the variable in, and there's no other way to get a connection.) + Just thrID <- readMVar (connThread c) + killConnection c Abnormal (CE.toException e) thrID + ) -- wait for connection_close_ok by the server; this MVar gets filled in the CE.finally handler in openConnection' readMVar $ connClosedLock c return () +-- | get the server properties sent in connection.start +getServerProperties :: Connection -> IO FieldTable +getServerProperties = return . connServerProperties + -- | @addConnectionClosedHandler conn ifClosed handler@ adds a @handler@ that will be called after the connection is closed (either by calling @closeConnection@ or by an exception). If the @ifClosed@ parameter is True and the connection is already closed, the handler will be called immediately. If @ifClosed == False@ and the connection is already closed, the handler will never be called addConnectionClosedHandler :: Connection -> Bool -> IO () -> IO () addConnectionClosedHandler conn ifClosed handler = do - withMVar (connClosed conn) $ \cc -> - case cc of - -- connection is already closed, so call the handler directly - Just _ | ifClosed == True -> handler + withMVar (connClosed conn) $ \case + -- connection is already closed, so call the handler directly + Just _ | ifClosed -> handler - -- otherwise add it to the list - _ -> modifyMVar_ (connClosedHandlers conn) $ \old -> return $ handler:old + -- otherwise add it to the list + _ -> modifyMVar_ (connClosedHandlers conn) $ \old -> return $ handler:old +-- | @addConnectionBlockedHandler conn blockedHandler unblockedHandler@ adds handlers that will be called +-- when a connection gets blocked/unlocked due to server resource constraints. +-- +-- More information: <https://www.rabbitmq.com/connection-blocked.html> +addConnectionBlockedHandler :: Connection -> (Text -> IO ()) -> IO () -> IO () +addConnectionBlockedHandler conn blockedHandler unblockedHandler = + modifyMVar_ (connBlockedHandlers conn) $ \old -> return $ (blockedHandler, unblockedHandler):old + readFrame :: Conn.Connection -> IO Frame readFrame handle = do strictDat <- connectionGetExact handle 7 @@ -456,7 +499,7 @@ | y == x = return bs | otherwise = do next <- Conn.connectionGet conn (x - y) - loop (BS.append bs next) (y + (BS.length next)) + loop (BS.append bs next) (y + BS.length next) writeFrame :: Conn.Connection -> Frame -> IO () writeFrame handle f = do @@ -467,25 +510,38 @@ {- | A connection to an AMQP server is made up of separate channels. It is recommended to use a separate channel for each thread in your application that talks to the AMQP server (but you don't have to as channels are thread-safe) -} data Channel = Channel { - connection :: Connection, - inQueue :: Chan FramePayload, --incoming frames (from Connection) - outstandingResponses :: MVar (Seq.Seq (MVar Assembly)), -- for every request an MVar is stored here waiting for the response - channelID :: Word16, - lastConsumerTag :: MVar Int, + connection :: Connection, + inQueue :: Chan FramePayload, -- incoming frames (from Connection) + outstandingResponses :: MVar (Seq.Seq (MVar Assembly)), -- for every request an MVar is stored here waiting for the response + channelID :: Word16, + lastConsumerTag :: MVar Int, - nextPublishSeqNum :: MVar Int, - unconfirmedSet :: TVar IntSet.IntSet, - ackedSet :: TVar IntSet.IntSet, --delivery tags - nackedSet :: TVar IntSet.IntSet, --accumulate here. + nextPublishSeqNum :: MVar 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 ()] - } + chanActive :: Lock, -- used for flow-control. if lock is closed, no content methods will be sent + chanClosed :: MVar (Maybe (CloseType, String)), + consumers :: MVar (M.Map Text ((Message, Envelope) -> IO (), -- who is consumer of a queue? (consumerTag => callback) + ConsumerTag -> IO ())), -- cancellation notification callback + returnListeners :: MVar [(Message, PublishError) -> IO ()], + confirmListeners :: MVar [(Word64, Bool, AckType) -> IO ()], + chanExceptionHandlers :: MVar [CE.SomeException -> IO ()] +} +-- | Thrown in the channel thread when the connection gets closed. +-- When handling exceptions in a subscription callback, make sure to re-throw this so the channel thread can be stopped. +data ChanThreadKilledException = ChanThreadKilledException { cause :: CE.SomeException } + deriving (Show) + +instance CE.Exception ChanThreadKilledException + +-- | If the given exception is an instance of ChanThreadKilledException, this method returns +-- the inner exception. Otherwise the exception is returned unchanged. +unwrapChanThreadKilledException :: CE.SomeException -> CE.SomeException +unwrapChanThreadKilledException e = maybe e cause $ CE.fromException e + msgFromContentHeaderProperties :: ContentHeaderProperties -> BL.ByteString -> Message msgFromContentHeaderProperties (CHBasic content_type content_encoding headers delivery_mode priority correlation_id reply_to expiration message_id timestamp message_type user_id application_id cluster_id) body = let msgId = fromShortString message_id @@ -523,12 +579,13 @@ channelReceiver chan where isResponse :: Assembly -> Bool - isResponse (ContentMethod (Basic_deliver _ _ _ _ _) _ _) = False - isResponse (ContentMethod (Basic_return _ _ _ _) _ _) = False + isResponse (ContentMethod Basic_deliver{} _ _) = False + isResponse (ContentMethod Basic_return{} _ _) = False isResponse (SimpleMethod (Channel_flow _)) = False - isResponse (SimpleMethod (Channel_close _ _ _ _)) = False + isResponse (SimpleMethod Channel_close{}) = False isResponse (SimpleMethod (Basic_ack _ _)) = False - isResponse (SimpleMethod (Basic_nack _ _ _)) = False + isResponse (SimpleMethod Basic_nack{}) = False + isResponse (SimpleMethod (Basic_cancel _ _)) = False isResponse _ = True --Basic.Deliver: forward msg to registered consumer @@ -537,20 +594,27 @@ properties body) = withMVar (consumers chan) (\s -> do case M.lookup consumerTag s of - Just subscriber -> do + Just (subscriber, _) -> do let msg = msgFromContentHeaderProperties properties body let env = Envelope {envDeliveryTag = deliveryTag, envRedelivered = redelivered, envExchangeName = exchange, envRoutingKey = routingKey, envChannel = chan} - CE.catch (subscriber (msg, env)) - (\(e::CE.SomeException) -> hPutStrLn stderr $ "AMQP callback threw exception: " ++ show e) + CE.catches (subscriber (msg, env)) [ + CE.Handler (\(e::ChanThreadKilledException) -> CE.throwIO $ cause e), + CE.Handler (\(e::CE.SomeException) -> hPutStrLn stderr $ "AMQP callback threw exception: " ++ show e) + ] Nothing -> -- got a message, but have no registered subscriber; so drop it return () ) handleAsync (SimpleMethod (Channel_close _ (ShortString errorMsg) _ _)) = do - closeChannel' chan errorMsg - myThreadId >>= flip CE.throwTo (ConnectionClosedException . T.unpack $ errorMsg) + CE.catch (writeAssembly' chan (SimpleMethod Channel_close_ok)) + (\ (_ :: CE.IOException) -> + -- do nothing if connection is already closed + return () + ) + closeChannel' chan Abnormal errorMsg + myThreadId >>= flip CE.throwTo (ChannelClosedException Abnormal . T.unpack $ errorMsg) handleAsync (SimpleMethod (Channel_flow active)) = do if active then openLock $ chanActive chan @@ -558,80 +622,120 @@ -- in theory we should respond with flow_ok but rabbitMQ 1.7 ignores that, so it doesn't matter return () --Basic.return - handleAsync (ContentMethod basicReturn@(Basic_return _ _ _ _) props body) = do + handleAsync (ContentMethod basicReturn@Basic_return{} props body) = do let msg = msgFromContentHeaderProperties props body pubError = basicReturnToPublishError basicReturn 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 + 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 (SimpleMethod (Basic_cancel consumerTag _)) = handleCancel consumerTag 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 + 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) + 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 + 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 + handleCancel (ShortString consumerTag) = + withMVar (consumers chan) (\s -> do + case M.lookup consumerTag s of + Just (_, cancelCB) -> + CE.catch (cancelCB consumerTag) $ \(ex :: CE.SomeException) -> + hPutStrLn stderr $ "consumer cancellation listener "++show consumerTag++" on channel ["++show (channelID chan)++"] threw exception: "++ show ex + Nothing -> + -- got a cancellation notification, but have no registered subscriber; so drop it + return () + ) + basicReturnToPublishError (Basic_return code (ShortString errText) (ShortString exchange) (ShortString routingKey)) = let replyError = case code of 312 -> Unroutable errText 313 -> NoConsumers errText 404 -> NotFound errText - num -> error $ "unexpected return error code: " ++ (show num) + num -> error $ "unexpected return error code: " ++show num pubError = PublishError replyError (Just exchange) routingKey in pubError basicReturnToPublishError x = error $ "basicReturnToPublishError fail: "++show x --- | registers a callback function that is called whenever a message is returned from the broker ('basic.return'). +-- | Registers a callback function that is called whenever a message is returned from the broker ('basic.return'). addReturnListener :: Channel -> ((Message, PublishError) -> IO ()) -> IO () addReturnListener chan listener = do modifyMVar_ (returnListeners chan) $ \listeners -> return $ listener:listeners --- | registers a callback function that is called whenever a channel is closed by an exception. +-- | Registers a callback function that is called whenever a channel is closed by an exception. +-- +-- This method will always be called when a channel is closed, whether through normal means +-- ('closeChannel', 'closeConnection') or by some AMQP exception. +-- +-- You can use 'isNormalChannelClose' to figure out if the exception was normal or due to an +-- AMQP exception. addChannelExceptionHandler :: Channel -> (CE.SomeException -> IO ()) -> IO () addChannelExceptionHandler chan handler = do modifyMVar_ (chanExceptionHandlers chan) $ \handlers -> return $ handler:handlers +-- | This can be used with the exception passed to 'addChannelExceptionHandler'. +-- +-- Returns True if the argument is a 'ConnectionClosedException' or 'ChannelClosedException' that happened +-- normally (i.e. by the user calling 'closeChannel' or 'closeConnection') and not due to some +-- AMQP exception. +isNormalChannelClose :: CE.SomeException -> Bool +isNormalChannelClose e = case CE.fromException e :: Maybe AMQPException of + Just (ChannelClosedException Normal _) -> True + Just (ConnectionClosedException Normal _) -> True + _ -> False + -- closes the channel internally; but doesn't tell the server -closeChannel' :: Channel -> Text -> IO () -closeChannel' c reason = do +closeChannel' :: Channel -> CloseType -> Text -> IO () +closeChannel' c closeType reason = do modifyMVar_ (chanClosed c) $ \x -> do if isNothing x then do modifyMVar_ (connChannels $ connection c) $ \old -> do ret <- freeChannel (connChanAllocator $ connection c) $ fromIntegral $ channelID c - when (not ret) $ hPutStrLn stderr "closeChannel error: channel already freed" + unless ret $ hPutStrLn stderr "closeChannel error: channel already freed" return $ IM.delete (fromIntegral $ channelID c) old void $ killLock $ chanActive c killOutstandingResponses $ outstandingResponses c - return $ Just $ maybe (T.unpack reason) id x + return $ Just (closeType, T.unpack reason) else return x where killOutstandingResponses :: MVar (Seq.Seq (MVar a)) -> IO () killOutstandingResponses outResps = do modifyMVar_ outResps $ \val -> do F.mapM_ (\x -> tryPutMVar x $ error "channel closed") val + -- Intentionally put 'undefined' into the MVar, so that reading from it will throw. + -- This MVar will be read from the 'request' method, where we appropriately catch ErrorCall exceptions. return undefined -- | opens a new channel on the connection +-- +-- By default, if a channel is closed by an AMQP exception, this exception will be printed to stderr. You can prevent this behaviour by setting a custom exception handler (using 'addChannelExceptionHandler'). +-- +-- Example of adding an exception-handler: +-- +-- > chan <- openChannel conn +-- > addChannelExceptionHandler chan $ \e -> do +-- > unless (isNormalChannelClose e) $ do +-- > putStrLn $ "channel exception: "++show e openChannel :: Connection -> IO Channel openChannel c = do newInQueue <- newChan @@ -648,71 +752,86 @@ cnfListeners <- newMVar [] handlers <- newMVar [] - --add new channel to connection's channel map + -- 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 nxtSeq unconfSet aSet nSet ca closed conss retListeners cnfListeners handlers thrID <- forkFinally' (channelReceiver newChannel) $ \res -> do - closeChannel' newChannel "closed" - case res of - Right _ -> return () - Left ex -> readMVar handlers >>= mapM_ ($ ex) + closeChannel' newChannel Normal "closed" + case res of + Right _ -> return () + Left ex -> do + let unwrappedExc = unwrapChanThreadKilledException ex + handlers' <- readMVar handlers + + case (null handlers', fromAbnormalChannelClose unwrappedExc) of + (True, Just reason) -> hPutStrLn stderr $ "unhandled AMQP channel exception (chanId="++show newChannelID++"): "++reason + _ -> mapM_ ($ unwrappedExc) handlers' when (IM.member newChannelID mp) $ CE.throwIO $ userError "openChannel fail: channel already open" return (IM.insert newChannelID (newChannel, thrID) mp, newChannel) - SimpleMethod (Channel_open_ok _) <- request newChannel (SimpleMethod (Channel_open (ShortString ""))) + SimpleMethod (Channel_open_ok _) <- request newChannel $ SimpleMethod $ Channel_open (ShortString "") return newChannel + where + fromAbnormalChannelClose :: CE.SomeException -> Maybe String + fromAbnormalChannelClose exc = + case CE.fromException exc :: Maybe AMQPException of + Just (ConnectionClosedException _ _) -> Nothing + Just (ChannelClosedException Normal _) -> Nothing + Just (ChannelClosedException Abnormal reason) -> Just reason + Just (AllChannelsAllocatedException _) -> Just "all channels allocated" + Nothing -> Just $ show exc + -- | closes a channel. It is typically not necessary to manually call this as closing a connection will implicitly close all channels. closeChannel :: Channel -> IO () closeChannel c = do SimpleMethod Channel_close_ok <- request c $ SimpleMethod $ Channel_close 0 (ShortString "") 0 0 withMVar (connChannels $ connection c) $ \chans -> do case IM.lookup (fromIntegral $ channelID c) chans of - Just (_, thrID) -> killThread thrID + Just (_, thrID) -> throwTo thrID $ ChannelClosedException Normal "closeChannel was called" Nothing -> return () -- | writes multiple frames to the channel atomically writeFrames :: Channel -> [FramePayload] -> IO () -writeFrames chan payloads = - let conn = connection chan in - withMVar (connChannels conn) $ \chans -> - if IM.member (fromIntegral $ channelID chan) chans - then - CE.catch - -- ensure at most one thread is writing to the socket at any time - (do - withMVar (connWriteLock conn) $ \_ -> - mapM_ (\payload -> writeFrame (connHandle conn) (Frame (channelID chan) payload)) payloads - updateLastSent conn) - ( \(_ :: CE.IOException) -> do - CE.throwIO $ userError "connection not open" - ) - else do - CE.throwIO $ userError "channel not open" +writeFrames chan payloads = do + let conn = connection chan + withMVar (connChannels conn) $ \chans -> + if IM.member (fromIntegral $ channelID chan) chans + then CE.catch + -- ensure at most one thread is writing to the socket at any time + (do + withMVar (connWriteLock conn) $ \_ -> + mapM_ (\payload -> writeFrame (connHandle conn) (Frame (channelID chan) payload)) payloads + updateLastSent conn) + (\(_ :: CE.IOException) -> do + CE.throwIO $ userError "connection not open" + ) + else do + CE.throwIO $ userError "channel not open" writeAssembly' :: Channel -> Assembly -> IO () writeAssembly' chan (ContentMethod m properties msg) = do -- wait iff the AMQP server instructed us to withhold sending content data (flow control) waitLock $ chanActive chan - let !toWrite = - [(MethodPayload m), - (ContentHeaderPayload + let !toWrite = [ + MethodPayload m, + ContentHeaderPayload (getClassIDOf properties) --classID 0 --weight is deprecated in AMQP 0-9 (fromIntegral $ BL.length msg) --bodySize - properties)] ++ + properties] ++ (if BL.length msg > 0 then do -- split into frames of maxFrameSize -- (need to substract 8 bytes to account for frame header and end-marker) map ContentBodyPayload - (splitLen msg $ (fromIntegral $ connMaxFrameSize $ connection chan) - 8) + (splitLen msg $ fromIntegral (connMaxFrameSize $ connection chan) - 8) else [] ) writeFrames chan toWrite where - splitLen str len | BL.length str > len = (BL.take len str):(splitLen (BL.drop len str) len) + splitLen str len | BL.length str > len = BL.take len str : splitLen (BL.drop len str) len splitLen str _ = [str] writeAssembly' chan (SimpleMethod m) = writeFrames chan [MethodPayload m] @@ -754,17 +873,16 @@ throwMostRelevantAMQPException chan = do cc <- readMVar $ connClosed $ connection chan case cc of - Just r -> CE.throwIO $ ConnectionClosedException r + Just (closeType, r) -> CE.throwIO $ ConnectionClosedException closeType r Nothing -> do chc <- readMVar $ chanClosed chan case chc of - Just r -> CE.throwIO $ ChannelClosedException r - Nothing -> CE.throwIO $ ConnectionClosedException "unknown reason" + Just (ct, r) -> CE.throwIO $ ChannelClosedException ct r + Nothing -> CE.throwIO $ ConnectionClosedException Abnormal "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 + pending <- readTVar $ unconfirmedSet chan + check (IntSet.null pending) + (,) <$> swapTVar (ackedSet chan) IntSet.empty + <*> swapTVar (nackedSet chan) IntSet.empty
Network/AMQP/Lifted.hs view
@@ -12,10 +12,7 @@ import Data.Text (Text) import Control.Monad --- | @consumeMsgs chan queueName ack callback@ subscribes to the given queue and returns a consumerTag. For any incoming message, the callback will be run. If @ack == 'Ack'@ you will have to acknowledge all incoming messages (see 'ackMsg' and 'ackEnv') --- --- NOTE: The callback will be run on the same thread as the channel thread (every channel spawns its own thread to listen for incoming data) so DO NOT perform any request on @chan@ inside the callback (however, you CAN perform requests on other open channels inside the callback, though I wouldn't recommend it). --- Functions that can safely be called on @chan@ are 'ackMsg', 'ackEnv', 'rejectMsg', 'recoverMsgs'. If you want to perform anything more complex, it's a good idea to wrap it inside 'forkIO'. +-- | Lifted version of 'Network.AMQP.consumeMsgs' (please look there for documentation). -- -- In addition, while the callback function @(('Message', 'Envelope') -> m ())@ -- has access to the captured state, all its side-effects in m are discarded. @@ -29,14 +26,15 @@ liftBaseWith $ \runInIO -> A.consumeMsgs chan queueName ack (void . runInIO . callback) --- | an extended version of @consumeMsgs@ that allows you to include arbitrary arguments. +-- | an extended version of @consumeMsgs@ that allows you to define a consumer cancellation callback and include arbitrary arguments. consumeMsgs' :: MonadBaseControl IO m => A.Channel -> Text -- ^ Specifies the name of the queue to consume from. -> A.Ack -> ((A.Message, A.Envelope) -> m ()) + -> (ConsumerTag -> m ()) -> FieldTable -> m A.ConsumerTag -consumeMsgs' chan queueName ack callback args = +consumeMsgs' chan queueName ack callback cancelled args = liftBaseWith $ \runInIO -> - A.consumeMsgs' chan queueName ack (void . runInIO . callback) args + A.consumeMsgs' chan queueName ack (void . runInIO . callback) (void . runInIO . cancelled) args
Network/AMQP/Protocol.hs view
@@ -12,9 +12,9 @@ --True if a content (contentheader and possibly contentbody) will follow the method hasContent :: FramePayload -> Bool -hasContent (MethodPayload (Basic_get_ok _ _ _ _ _)) = True -hasContent (MethodPayload (Basic_deliver _ _ _ _ _)) = True -hasContent (MethodPayload (Basic_return _ _ _ _)) = True +hasContent (MethodPayload Basic_get_ok{}) = True +hasContent (MethodPayload Basic_deliver{}) = True +hasContent (MethodPayload Basic_return{}) = True hasContent _ = False data Frame = Frame ChannelID FramePayload --channel, payload @@ -42,20 +42,20 @@ peekFrameSize = runGet f where f = do - void $ getWord8 -- 1 byte - void $ (get :: Get ChannelID) -- 2 bytes - get :: Get PayloadSize -- 4 bytes + void getWord8 -- 1 byte + void (get :: Get ChannelID) -- 2 bytes + get :: Get PayloadSize -- 4 bytes data FramePayload = - MethodPayload MethodPayload - | ContentHeaderPayload ShortInt ShortInt LongLongInt ContentHeaderProperties --classID, weight, bodySize, propertyFields - | ContentBodyPayload BL.ByteString - | HeartbeatPayload + MethodPayload MethodPayload + | ContentHeaderPayload ShortInt ShortInt LongLongInt ContentHeaderProperties --classID, weight, bodySize, propertyFields + | ContentBodyPayload BL.ByteString + | HeartbeatPayload deriving Show frameType :: FramePayload -> Word8 frameType (MethodPayload _) = 1 -frameType (ContentHeaderPayload _ _ _ _) = 2 +frameType ContentHeaderPayload{} = 2 frameType (ContentBodyPayload _) = 3 frameType HeartbeatPayload = 8 @@ -76,6 +76,7 @@ -- ignoring the actual payload, but still need to read the bytes from the network buffer _ <- getLazyByteString $ fromIntegral payloadSize return HeartbeatPayload +-- this should never happen: getPayload n _ = error ("Unknown frame payload: " ++ show n) putPayload :: FramePayload -> Put @@ -86,4 +87,4 @@ put bodySize putContentHeaderProperties p putPayload (ContentBodyPayload payload) = putLazyByteString payload -putPayload HeartbeatPayload = putLazyByteString BL.empty+putPayload HeartbeatPayload = putLazyByteString BL.empty
Network/AMQP/Types.hs view
@@ -13,12 +13,14 @@ LongLongInt, ShortString(..), LongString(..), + ConsumerTag, Timestamp, FieldTable(..), FieldValue(..), Decimals, DecimalValue(..), ConfirmationResult(..), + CloseType(..) ) where import Control.Applicative @@ -38,11 +40,14 @@ import qualified Data.Map as M import qualified Data.Text.Encoding as T +-- | describes whether a channel was closed by user-request (Normal) or by an AMQP exception (Abnormal) +data CloseType = Normal | Abnormal + deriving (Typeable, Show, Ord, Eq) data AMQPException = - -- | the 'String' contains the reason why the channel was closed - ChannelClosedException String - | ConnectionClosedException String -- ^ String may contain a reason + -- | the 'String' contains the reason why the channel was closed + ChannelClosedException CloseType String + | ConnectionClosedException CloseType String -- ^ String may contain a reason | AllChannelsAllocatedException Int -- ^ the 'Int' contains the channel-max property of the connection (i.e. the highest permitted channel id) deriving (Typeable, Show, Ord, Eq) instance CE.Exception AMQPException @@ -75,13 +80,15 @@ type LongInt = Word32 type LongLongInt = Word64 +type ConsumerTag = Text + newtype ShortString = ShortString Text deriving (Eq, Ord, Read, Show) instance Binary ShortString where get = do - len <- getWord8 - dat <- getByteString (fromIntegral len) - return $ ShortString $ T.decodeUtf8 dat + len <- getWord8 + dat <- getByteString (fromIntegral len) + return $ ShortString $ T.decodeUtf8 dat put (ShortString x) = do let s = T.encodeUtf8 x @@ -95,9 +102,9 @@ deriving (Eq, Ord, Read, Show) instance Binary LongString where get = do - len <- getWord32be - dat <- getByteString (fromIntegral len) - return $ LongString dat + len <- getWord32be + dat <- getByteString (fromIntegral len) + return $ LongString dat put (LongString x) = do putWord32be $ fromIntegral (BS.length x) @@ -118,7 +125,7 @@ fvp <- getLazyByteString (fromIntegral len) let !fields = readMany fvp return $ FieldTable $ M.fromList $ map (\(ShortString a, b) -> (a,b)) fields - else return $ FieldTable $ M.empty + else return $ FieldTable M.empty put (FieldTable fvp) = do let bytes = runPut (putMany $ map (\(a,b) -> (ShortString a, b)) $ M.toList fvp) :: BL.ByteString @@ -135,7 +142,7 @@ | FVFloat Float | FVDouble Double | FVDecimal DecimalValue - | FVString Text + | FVString BS.ByteString | FVFieldArray [FieldValue] | FVTimestamp Timestamp | FVFieldTable FieldTable @@ -157,7 +164,7 @@ 'D' -> FVDecimal <$> get 'S' -> do LongString x <- get :: Get LongString - return $ FVString $ T.decodeUtf8 x + return $ FVString x 'A' -> do len <- get :: Get Int32 if len > 0 @@ -172,6 +179,7 @@ 'x' -> do len <- get :: Get Word32 FVByteArray <$> getByteString (fromIntegral len) + -- this should never happen: c -> error ("Unknown field type: " ++ show c) put (FVBool x) = put 't' >> put x @@ -182,10 +190,10 @@ put (FVFloat x) = put 'f' >> putFloat32be x put (FVDouble x) = put 'd' >> putFloat64be x put (FVDecimal x) = put 'D' >> put x - put (FVString x) = put 'S' >> put (LongString $ T.encodeUtf8 x) + put (FVString x) = put 'S' >> put (LongString x) put (FVFieldArray x) = do put 'A' - if length x == 0 + if null x then put (0 :: Int32) else do let bytes = runPut (putMany x) :: BL.ByteString @@ -193,7 +201,7 @@ putLazyByteString bytes put (FVTimestamp s) = put 'T' >> put s put (FVFieldTable s) = put 'F' >> put s - put (FVVoid) = put 'V' + put FVVoid = put 'V' put (FVByteArray x) = do put 'x' let len = fromIntegral (BS.length x) :: Word32
amqp.cabal view
@@ -1,5 +1,5 @@ Name: amqp -Version: 0.14.1 +Version: 0.24.0 Synopsis: Client library for AMQP servers (currently only RabbitMQ) Description: Client library for AMQP servers (currently only RabbitMQ) License: BSD3 @@ -11,39 +11,37 @@ Build-Type: Simple Homepage: https://github.com/hreinhardt/amqp bug-reports: https://github.com/hreinhardt/amqp/issues -Cabal-Version: >=1.8 +Cabal-Version: >=1.10 Extra-source-files: examples/ExampleConsumer.hs, examples/ExampleProducer.hs, changelog.md -flag network-uri - description: Get Network.URI from the network-uri package - default: True - Library + default-language: Haskell2010 Build-Depends: base >= 4 && < 5, binary >= 0.5, containers>=0.2, bytestring>=0.9, data-binary-ieee754>=0.4.2.1, + data-default-class >= 0.1, text>=0.11.2, split>=0.2, clock >= 0.4.0.1, monad-control >= 0.3, - connection == 0.2.*, + crypton-connection >= 0.4 && <= 0.5, vector, - stm >= 2.4.0 - if flag(network-uri) - build-depends: network-uri >= 2.6, network > 2.6 - else - build-depends: network < 2.6 + stm >= 2.4.0, + network-uri >= 2.6, + network > 2.6 + Exposed-modules: Network.AMQP, Network.AMQP.Types, Network.AMQP.Lifted Other-modules: Network.AMQP.ChannelAllocator, Network.AMQP.Generated, Network.AMQP.Helpers, Network.AMQP.Protocol, Network.AMQP.Internal, Paths_amqp GHC-Options: -Wall Executable amqp-builder + default-language: Haskell2010 Build-Depends: base >= 4 && < 5, xml == 1.3.*, containers >= 0.2 Hs-Source-Dirs: Tools Main-is: Builder.hs @@ -54,6 +52,7 @@ location: https://github.com/hreinhardt/amqp test-suite spec + default-language: Haskell2010 type: exitcode-stdio-1.0 ghc-options: @@ -69,6 +68,7 @@ ConnectionSpec ExchangeDeclareSpec ExchangeDeleteSpec + FromURISpec QueueDeclareSpec QueueDeleteSpec QueuePurgeSpec @@ -78,15 +78,14 @@ , containers>=0.2 , bytestring>=0.9 , data-binary-ieee754>=0.4.2.1 + , data-default-class >= 0.1 , text>=0.11.2 , split>=0.2 , clock >= 0.4.0.1 , hspec >= 1.3 , hspec-expectations >= 0.3.3 - , connection == 0.2.* + , crypton-connection >= 0.4 && <= 0.5 , vector , stm >= 2.4.0 - if flag(network-uri) - build-depends: network-uri >= 2.6, network > 2.6 - else - build-depends: network < 2.6 + , network-uri >= 2.6 + , network > 2.6
changelog.md view
@@ -1,3 +1,76 @@+### Version 0.24.0 + +* the `fromURI` method now returns `Either String ConnectionOpts` to better handle parsing-errors + +### Version 0.23.0 + +* bump dependency bounds for `crypton-connection` + +### Version 0.22.2 + +* replace `connection` dependency with `crypton-connection` + +### Version 0.22.1 + +* fix a potential deadlock in `closeConnection` + +### Version 0.22.0 + +* `closeChannel` now uses a `ChannelClosedException` to kill the channel-thread instead of `ThreadKilled`. This fixes a bug in 0.21.0 that led to errors being printed on the screen when `closeChannel` was called. + +### Version 0.21.0 + +* added method `isNormalChannelClose` +* Channels that have no explicit exception handlers (added using `addChannelExceptionHandler`) will now print all exceptions to `stderr`, even if those are not of type `ChannelClosedException`. This is an uncommon situation, so it should not have an observable effect for most users. + +### Version 0.20.0 + +* `fromURI` now activates TLS if the URI starts with `ampqs://`. Previously it only changed the port, without activating TLS. +* `fromURI` now supports multi-broker URIs in the form of `amqp://user:pass@host-1:port-2,host-2:port-2/vhost` + +### Version 0.19.1 + +* add `nackMsg` and `nackEnv` methods + +### Version 0.19.0 + +* change `FVString` to be binary instead of UTF-8 encoded + +### Version 0.18.3 + +* respond to `channel.close` messages from the server with `channel.close_ok` + +### Version 0.18.2 + +* support `connection` 0.3 and drop support for `network` < 2.6 + +### Version 0.18.1 + +* new function `addConnectionBlockedHandler` to be notified when the connection is blocked (due to the server being resource-constrained) + +### Version 0.18.0 + +* `ConnectionClosedException` and `ChannelClosedException` now specify whether the close was normal (user-initiated) or abnormal (caused by an AMQP exception) +* channels that are abnormally closed and have no exception handler (set using `addChannelExceptionHandler`) will now print the error to `stderr` +* new function `getServerProperties` to get the RabbitMQ server-properties +* `consumeMsgs'` now allows setting a consumer-cancellation-callback + +### Version 0.17.0 + +* When the server asynchronously closed a channel, this was (erroneously) internally represented as a `ConnectionClosedException`. It is now represented as a `ChannelClosedException`. This could affect you if you explicitly match on `ConnectionClosedException` or `ChannelClosedException` in your code, for example when using `addChannelExceptionHandler`. + +### Version 0.16.0 + +* new `coName` field in `ConnectionOpts` to specify a custom name that will be displayed in the RabbitMQ web interface + +### Version 0.15.1 + +* export the `AckType` data-type and constructors + +### Version 0.15.0 + +* The way channels are closed internally was changed. This may affect you if you have installed an exception handler inside the callback passed to `consumeMsgs`. Specifically, the exceptions used internally to close channels are now wrapped inside `ChanThreadKilledException`. You should make sure to re-throw this exception if you did catch it. + ### Version 0.14.1 * show all exceptions if no host can be connected to
examples/ExampleConsumer.hs view
@@ -1,4 +1,6 @@ {-# OPTIONS -XOverloadedStrings #-} +module ExampleConsumer where + import Network.AMQP import qualified Data.ByteString.Lazy.Char8 as BL
examples/ExampleProducer.hs view
@@ -1,4 +1,6 @@ {-# OPTIONS -XOverloadedStrings #-} +module ExampleProducer where + import Network.AMQP import qualified Data.ByteString.Lazy.Char8 as BL
test/BasicPublishSpec.hs view
@@ -6,8 +6,12 @@ import Network.AMQP import Data.ByteString.Lazy.Char8 as BL +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Word import Control.Concurrent (threadDelay) +import Control.Concurrent.STM main :: IO () main = hspec spec @@ -60,3 +64,66 @@ _ <- deleteQueue ch q closeConnection conn + context "confirmSelect" $ do + it "receives a confirmation message" $ do + let q = "haskell-amqp.queues.publish-over-fanout1" + e = "haskell-amqp.fanout.d.na" + conn <- openConnection "127.0.0.1" "/" "guest" "guest" + ch <- openChannel conn + confirmSelect ch True + (confirmMap, counter) <- atomically $ (,) <$> newTVar Map.empty <*> newTVar 0 + addConfirmationListener ch (handleConfirms counter confirmMap) + _ <- declareExchange ch (newExchange {exchangeName = e, + exchangeType = "fanout", + exchangeDurable = True}) + + (_, _, _) <- declareQueue ch (newQueue {queueName = q, queueDurable = False}) + _ <- purgeQueue ch q + bindQueue ch q e "" + + + _ <- traverse (\n -> do + sn' <- publishMsg ch e "" (newMsg {msgBody = (BL.pack "hello")}) + case sn' of + Just sn -> atomically $ addSequenceNumber confirmMap (fromIntegral sn) n + Nothing -> return () + + ) [1..5] + + + threadDelay (1000 * 100) + + (_, n, _) <- declareQueue ch (newQueue {queueName = q, queueDurable = False}) + n `shouldBe` 5 + + cMap' <- atomically $ readTVar confirmMap + cMap' `shouldBe` Map.empty + + counter' <- atomically $ readTVar counter + counter' `shouldBe` 5 + + _ <- deleteQueue ch q + closeConnection conn + + +addSequenceNumber :: TVar (Map Word64 Int) -> Word64 -> Int -> STM () +addSequenceNumber cMap sn n = modifyTVar' cMap (Map.insert sn n) + +removeSequenceNumber :: TVar (Map Word64 Int) -> Word64 -> STM () +removeSequenceNumber cMap sn = modifyTVar' cMap (Map.delete sn) + +increaseCounter :: TVar Int -> STM () +increaseCounter n = modifyTVar' n (+1) + +handleConfirms :: TVar Int -> TVar (Map Word64 Int) -> (Word64, Bool, AckType) -> IO () +handleConfirms c _ (_, False, BasicNack) = atomically $ increaseCounter c +handleConfirms c _ (_, True, BasicNack) = atomically $ increaseCounter c +handleConfirms c cMap (n, False, BasicAck) = atomically $ removeSequenceNumber cMap n >> increaseCounter c +handleConfirms c cMap (n, True, BasicAck) = atomically $ do + cMap' <- readTVar cMap + let (lt, eq', _) = Map.splitLookup n cMap' + case eq' of + Just _ -> removeSequenceNumber cMap n >> increaseCounter c + Nothing -> return () + _ <- traverse (\i -> removeSequenceNumber cMap i >> increaseCounter c) (Map.keys lt) + return ()
test/ConnectionSpec.hs view
@@ -26,7 +26,7 @@ context "with custom vhost and valid credentials" $ do it "raises an exception" $ do - let ex = ConnectionClosedException "Handshake failed. Please check the RabbitMQ logs for more information" + let ex = ConnectionClosedException Abnormal "Handshake failed. Please check the RabbitMQ logs for more information" (openConnection "127.0.0.1" "haskell_amqp_testbed" "NxqrbLaNiN3TAenNu:r9Pq]XwABuRs" "RyRxfVDyrKjC8yhJ6htCp}P>FnJxfc") `shouldThrow` (== ex)
test/ExchangeDeclareSpec.hs view
@@ -50,8 +50,11 @@ conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () + let x = "haskell-amqp.exchanges.Xiz2mQozyYcFrQgGmN8r" - ex = ChannelClosedException "NOT_FOUND - no exchange 'haskell-amqp.exchanges.Xiz2mQozyYcFrQgGmN8r' in vhost '/'" + ex = ChannelClosedException Abnormal "NOT_FOUND - no exchange 'haskell-amqp.exchanges.Xiz2mQozyYcFrQgGmN8r' in vhost '/'" (declareExchange ch $ newExchange {exchangeName = x, exchangePassive = True}) `shouldThrow` (== ex) closeConnection conn
test/ExchangeDeleteSpec.hs view
@@ -17,14 +17,17 @@ conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () + _ <- declareExchange ch (newExchange {exchangeName = eName, exchangeType = "topic", exchangeDurable = False}) _ <- deleteExchange ch eName - let ex = ChannelClosedException "NOT_FOUND - no exchange 'haskell-amqp.exchanges.to-be-deleted' in vhost '/'" + let ex = ChannelClosedException Abnormal "NOT_FOUND - no exchange 'haskell-amqp.exchanges.to-be-deleted' in vhost '/'" (declareExchange ch $ newExchange {exchangeName = eName, exchangePassive = True}) `shouldThrow` (== ex) closeConnection conn @@ -33,9 +36,11 @@ it "throws an exception" $ do conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () let q = "haskell-amqp.exchanges.GmN8rozyXiz2mQYcFrQg" - ex = ChannelClosedException "NOT_FOUND - no exchange 'haskell-amqp.exchanges.GmN8rozyXiz2mQYcFrQg' in vhost '/'" + ex = ChannelClosedException Abnormal "NOT_FOUND - no exchange 'haskell-amqp.exchanges.GmN8rozyXiz2mQYcFrQg' in vhost '/'" (declareExchange ch $ newExchange {exchangeName = q, exchangePassive = True}) `shouldThrow` (== ex) closeConnection conn
+ test/FromURISpec.hs view
@@ -0,0 +1,190 @@+{-# OPTIONS -XOverloadedStrings #-} + + +module FromURISpec (main, spec) where + + +import Network.AMQP +import Test.Hspec + + +main :: IO () +main = hspec spec + +spec :: Spec +spec = do + describe "fromURI" $ do + it "empty" $ do + let Right o = fromURI "" + coServers o `shouldBe` [("localhost", 5672)] + coVHost o `shouldBe` "/" + -- avoid undefined SASLMechanism Show instance + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULguest\NULguest"] + -- avoid undefined TLSSettings Show instance + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1" + coServers o `shouldBe` [("127.0.0.1", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host-2" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1,127.0.0.2" + coServers o `shouldBe` [("127.0.0.1", 5672),("127.0.0.2", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host port" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1:5672" + coServers o `shouldBe` [("127.0.0.1", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host-2 port" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1,127.0.0.2:5672" + coServers o `shouldBe` [("127.0.0.1", 5672),("127.0.0.2", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host-2 port-2" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1:5672,127.0.0.2:5672" + coServers o `shouldBe` [("127.0.0.1", 5672),("127.0.0.2", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host port vhost" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1:5672/v" + coServers o `shouldBe` [("127.0.0.1", 5672)] + coVHost o `shouldBe` "v" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host-2 port-2 vhost" $ do + let Right o = fromURI "amqp://u:p@127.0.0.1:5673,127.0.0.2:5674/v" + coServers o `shouldBe` [("127.0.0.1", 5673),("127.0.0.2", 5674)] + coVHost o `shouldBe` "v" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp auth host-3 port-3 vhost" $ do + let Right o = fromURI "amqp://a:b@h1:8001,h2:8002,h3:8003/w" + coServers o `shouldBe` [("h1", 8001),("h2", 8002),("h3", 8003)] + coVHost o `shouldBe` "w" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULa\NULb"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp host" $ do + let Right o = fromURI "amqp://127.0.0.1" + -- this appears to break: user host + coServers o `shouldBe` [("localhost", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` + ["\NUL127.0.0.1\NULguest"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqp host-2" $ do + let Right o = fromURI "amqp://127.0.0.1,127.0.0.2" + -- this appears to break: user host + coServers o `shouldBe` [("localhost", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` + ["\NUL127.0.0.1,127.0.0.2\NULguest"] + (isTrusted $ coTLSSettings o) `shouldBe` False + + it "amqps auth host" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1" + coServers o `shouldBe` [("127.0.0.1", 5671)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host-2" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1,127.0.0.2" + coServers o `shouldBe` [("127.0.0.1", 5671),("127.0.0.2", 5671)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host port" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1:5672" + coServers o `shouldBe` [("127.0.0.1", 5672)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host-2 port" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1,127.0.0.1:5673" + coServers o `shouldBe` [("127.0.0.1", 5671),("127.0.0.1", 5673)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host-2 port-2" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1:5672,127.0.0.1:5673" + coServers o `shouldBe` [("127.0.0.1", 5672),("127.0.0.1", 5673)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host port vhost" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1:5672/v" + coServers o `shouldBe` [("127.0.0.1", 5672)] + coVHost o `shouldBe` "v" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps auth host-2 port-2 vhost" $ do + let Right o = fromURI "amqps://u:p@127.0.0.1:5672,127.0.0.2:5673/v" + coServers o `shouldBe` [("127.0.0.1", 5672),("127.0.0.2", 5673)] + coVHost o `shouldBe` "v" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` ["\NULu\NULp"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps host" $ do + let Right o = fromURI "amqps://127.0.0.1" + -- this appears to break: user host + coServers o `shouldBe` [("localhost", 5671)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` + ["\NUL127.0.0.1\NULguest"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + it "amqps host-2" $ do + let Right o = fromURI "amqps://127.0.0.1,127.0.0.2" + -- this appears to break: user host + coServers o `shouldBe` [("localhost", 5671)] + coVHost o `shouldBe` "/" + (saslName <$> coAuth o) `shouldBe` ["PLAIN"] + (saslInitialResponse <$> coAuth o) `shouldBe` + ["\NUL127.0.0.1,127.0.0.2\NULguest"] + (isTrusted $ coTLSSettings o) `shouldBe` True + + +isTrusted :: Maybe TLSSettings -> Bool +isTrusted (Just TLSTrusted) = True +isTrusted _ = False
test/QueueDeclareSpec.hs view
@@ -48,7 +48,7 @@ -- consumer count, undelivered message count cn `shouldBe` 0 mn `shouldBe` 0 - + closeConnection conn context "passive declaration when the queue DOES NOT exist" $ do @@ -56,8 +56,11 @@ conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () + let x = "haskell-amqp.queues.mQozyYcFrQgGmN8Xiz2r" - ex = ChannelClosedException "NOT_FOUND - no queue 'haskell-amqp.queues.mQozyYcFrQgGmN8Xiz2r' in vhost '/'" + ex = ChannelClosedException Abnormal "NOT_FOUND - no queue 'haskell-amqp.queues.mQozyYcFrQgGmN8Xiz2r' in vhost '/'" (declareQueue ch $ newQueue {queueName = x, queuePassive = True}) `shouldThrow` (== ex) closeConnection conn
test/QueueDeleteSpec.hs view
@@ -15,6 +15,8 @@ it "deletes the queue" $ do conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () (q, _, _) <- declareQueue ch (newQueue {queueName = "haskell-amqp.queues.to-be-deleted", queueExclusive = True}) @@ -23,7 +25,7 @@ n <- deleteQueue ch q n `shouldBe` 0 - let ex = (ChannelClosedException "NOT_FOUND - no queue 'haskell-amqp.queues.to-be-deleted' in vhost '/'") + let ex = (ChannelClosedException Abnormal "NOT_FOUND - no queue 'haskell-amqp.queues.to-be-deleted' in vhost '/'") (declareQueue ch $ newQueue {queueName = q, queuePassive = True}) `shouldThrow` (== ex) closeConnection conn @@ -32,9 +34,11 @@ it "throws an exception" $ do conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () let q = "haskell-amqp.queues.GmN8rozyXiz2mQYcFrQg" - ex = ChannelClosedException "NOT_FOUND - no queue 'haskell-amqp.queues.GmN8rozyXiz2mQYcFrQg' in vhost '/'" + ex = ChannelClosedException Abnormal "NOT_FOUND - no queue 'haskell-amqp.queues.GmN8rozyXiz2mQYcFrQg' in vhost '/'" (declareQueue ch $ newQueue {queueName = q, queuePassive = True}) `shouldThrow` (== ex) closeConnection conn
test/QueuePurgeSpec.hs view
@@ -43,8 +43,10 @@ it "empties the queue" $ do conn <- openConnection "127.0.0.1" "/" "guest" "guest" ch <- openChannel conn + -- silence error messages + addChannelExceptionHandler ch $ return . const () - let ex = ChannelClosedException "NOT_FOUND - no queue 'haskell-amqp.queues.avjqmyG{CHrc66MRyzYVA+PwrMVARJ' in vhost '/'" + let ex = ChannelClosedException Abnormal "NOT_FOUND - no queue 'haskell-amqp.queues.avjqmyG{CHrc66MRyzYVA+PwrMVARJ' in vhost '/'" (purgeQueue ch "haskell-amqp.queues.avjqmyG{CHrc66MRyzYVA+PwrMVARJ") `shouldThrow` (== ex) closeConnection conn
test/Runner.hs view
@@ -2,6 +2,7 @@ import qualified ConnectionSpec import qualified ChannelSpec +import qualified FromURISpec import qualified QueueDeclareSpec import qualified QueueDeleteSpec import qualified QueuePurgeSpec @@ -21,6 +22,8 @@ describe "ExchangeDeclareSpec" ExchangeDeclareSpec.spec -- exchange.delete describe "ExchangeDeleteSpec" ExchangeDeleteSpec.spec + -- fromuri.* + describe "FromURISpec" FromURISpec.spec -- queue.declare describe "QueueDeclareSpec" QueueDeclareSpec.spec -- queue.delete