sqsd-local-0.2.0: src/Network/SQS/Daemon/Local.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
module Network.SQS.Daemon.Local
( startDaemon
, DaemonOptions(..)
, workerUrl
, workerQueueName
, deadLetterQueueName
, httpTimeout
, contentType
, sqsHost
, sqsPort
, forked
) where
import Control.Concurrent.Lifted
import Control.Lens
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Control.Monad.Trans.AWS
import Control.Monad.Trans.Resource
import Data.ByteString (ByteString)
import Data.CaseInsensitive
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HM
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding
import Data.Text.IO as Text
import Network.AWS.SQS
import Network.HTTP.Client (defaultManagerSettings,
managerResponseTimeout)
import Network.Wreq
import System.IO
import Text.Printf
data DaemonOptions
= DaemonOptions { _workerUrl :: Text
, _workerQueueName :: Text
, _deadLetterQueueName :: Maybe Text
, _httpTimeout :: Maybe Int
, _contentType :: ByteString
, _sqsHost :: ByteString
, _sqsPort :: Int
, _forked :: Bool
} deriving (Show, Eq, Ord)
makeLenses ''DaemonOptions
data WithUrls o
= WithUrls { _daemonOptions :: o
, _workerQueueUrl :: Text
, _deadLetterQueueUrl :: Maybe Text
} deriving (Show, Eq, Ord)
makeLenses ''WithUrls
say :: MonadIO m => Text -> m ()
say = liftIO . Text.putStrLn
getQueueUrl :: (MonadCatch a, MonadResource a) => Text -> AWST a Text
getQueueUrl = fmap (view gqursQueueURL) . send . getQueueURL
addAttributeHeaders :: Options
-> HashMap Text MessageAttributeValue
-> Options
addAttributeHeaders =
HM.foldlWithKey' addHeader
where
addHeader opts' key attr =
case attr ^. mavStringValue of
Just value ->
let headerName = mk ("x-aws-sqsd-attr-" <> encodeUtf8 key)
in opts' & header headerName .~ [encodeUtf8 value]
Nothing -> opts'
deleteMessageByReceiptHandle :: (MonadCatch m, MonadResource m)
=> Text
-> Text
-> AWST m ()
deleteMessageByReceiptHandle url receiptHandle = do
void (send (deleteMessage url receiptHandle))
liftIO (printf "Deleted message from queue: %s\n" url)
sendToDeadLetterQueueAndDelete :: (MonadCatch m, MonadResource m)
=> WithUrls DaemonOptions
-> Text
-> Text
-> HashMap Text MessageAttributeValue
-> AWST m ()
sendToDeadLetterQueueAndDelete opts receiptHandle body attrs = do
-- Only send to dead letter queue if specified.
case opts ^. deadLetterQueueUrl of
Just dlqUrl -> do
let dlqMsg = sendMessage dlqUrl body
& smMessageAttributes .~ attrs
void (send dlqMsg)
say "Sent message to dead letter queue."
Nothing -> return ()
say "Deleting handled message from worker queue."
void (send (deleteMessage (opts ^. workerQueueUrl) receiptHandle))
forwardMessage :: (MonadCatch m, MonadResource m)
=> WithUrls DaemonOptions
-> Message
-> AWST m ()
forwardMessage o msg =
case (msg ^. mReceiptHandle, msg ^. mBody) of
(Just receiptHandle, Just body) -> do
let attrs = msg ^. mMessageAttributes
timeout' = (* 1000000) <$> o ^. daemonOptions . httpTimeout
opts = defaults `addAttributeHeaders` attrs
& header "content-type" .~ [o ^. daemonOptions . contentType]
& manager .~ Left (defaultManagerSettings { managerResponseTimeout = timeout' })
req = postWith opts (Text.unpack (o ^. daemonOptions . workerUrl)) (encodeUtf8 body)
result <- liftIO (try req)
case result of
Right res | res ^. responseStatus . statusCode == 200 ->
deleteMessageByReceiptHandle (o ^. workerQueueUrl) receiptHandle
Right res -> do
liftIO (printf "Worker failed with status code: %d\n" (res ^. responseStatus . statusCode))
sendToDeadLetterQueueAndDelete o receiptHandle body attrs
Left (e :: HttpException) -> do
liftIO (printf "Worker request failed: %s\n" (show e))
sendToDeadLetterQueueAndDelete o receiptHandle body attrs
(Nothing, _) -> say "Message had no reciept handle, ignoring."
(_, Nothing) -> say "Message had no body, ignoring."
receiveNext :: (MonadCatch m, MonadResource m, MonadBaseControl IO m)
=> WithUrls DaemonOptions
-> AWST m ()
receiveNext o = do
msgs <- view rmrsMessages
<$> send (receiveMessage (o ^. workerQueueUrl)
& rmMaxNumberOfMessages ?~ 10
& rmWaitTimeSeconds ?~ 20)
unless (Prelude.null msgs) $
let forkedMsg = if o ^. daemonOptions . forked
then "concurrent"
else "serialized"
in liftIO (printf "Received %d new messages, posting to worker (%s).\n"
(Prelude.length msgs)
(forkedMsg :: String))
let process = if o ^. daemonOptions . forked then void . fork else void
forM_ msgs (process . forwardMessage o)
startDaemon :: DaemonOptions
-> IO ()
startDaemon opts = do
lgr <- newLogger Info stdout
env <- newEnv Ireland Discover <&> set envLogger lgr
let sqsEndpoint = setEndpoint False (opts ^. sqsHost) (opts ^. sqsPort) sqs
runResourceT . runAWST env $
reconfigure sqsEndpoint $ do
o <- WithUrls opts
<$> getQueueUrl (opts ^. workerQueueName)
<*> maybe (return Nothing) (fmap Just . getQueueUrl) (opts ^. deadLetterQueueName)
when (isNothing (o ^. deadLetterQueueUrl)) $
say "No dead letter queue specified."
say "Starting local SQSD. Awaiting messages..."
void (forever (receiveNext o))