More tests, refactorings

parent ffa0f57f
Pipeline #6509 failed with stages
in 22 minutes and 9 seconds
...@@ -14,6 +14,7 @@ import Async.Worker.Types (State(..), PerformAction, getJob, formatStr, TimeoutS ...@@ -14,6 +14,7 @@ import Async.Worker.Types (State(..), PerformAction, getJob, formatStr, TimeoutS
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent (forkIO, threadDelay)
import Control.Exception (Exception, throwIO) import Control.Exception (Exception, throwIO)
import Control.Monad (void)
import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), withObject, withText) import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), withObject, withText)
import Data.Text qualified as T import Data.Text qualified as T
import Database.PostgreSQL.Simple qualified as PSQL import Database.PostgreSQL.Simple qualified as PSQL
...@@ -75,7 +76,7 @@ main :: IO () ...@@ -75,7 +76,7 @@ main :: IO ()
main = do main = do
let connInfo = PSQL.defaultConnectInfo { PSQL.connectUser = "postgres" let connInfo = PSQL.defaultConnectInfo { PSQL.connectUser = "postgres"
, PSQL.connectDatabase = "postgres" } , PSQL.connectDatabase = "postgres" }
let brokerInitParams = PGMQBrokerInitParams connInfo :: BrokerInitParams PGMQBroker (Job Message) let brokerInitParams = PGMQBrokerInitParams connInfo 10 :: BrokerInitParams PGMQBroker (Job Message)
let queue = "simple_worker" let queue = "simple_worker"
...@@ -108,17 +109,17 @@ main = do ...@@ -108,17 +109,17 @@ main = do
let mkJob msg = mkDefaultSendJob' broker queue msg let mkJob msg = mkDefaultSendJob' broker queue msg
mapM_ (\idx -> do mapM_ (\idx -> do
sendJob' $ mkJob $ Ping void $ sendJob' $ mkJob $ Ping
sendJob' $ mkJob $ Wait 1 void $ sendJob' $ mkJob $ Wait 1
sendJob' $ mkJob $ Echo $ "hello " <> show idx void $ sendJob' $ mkJob $ Echo $ "hello " <> show idx
sendJob' $ mkJob $ Error $ "error " <> show idx void $ sendJob' $ mkJob $ Error $ "error " <> show idx
) tasksLst ) tasksLst
-- a job that will timeout -- a job that will timeout
let timedOut = let timedOut =
(mkDefaultSendJob broker queue (Wait 5) 1) (mkDefaultSendJob broker queue (Wait 5) 1)
{ toStrat = TSRepeatNElseArchive 3 } { toStrat = TSRepeatNElseArchive 3 }
sendJob' timedOut void $ sendJob' timedOut
threadDelay (10*second) threadDelay (10*second)
......
...@@ -8,6 +8,6 @@ packages: ...@@ -8,6 +8,6 @@ packages:
source-repository-package source-repository-package
type: git type: git
location: https://gitlab.iscpif.fr/gargantext/haskell-pgmq location: https://gitlab.iscpif.fr/gargantext/haskell-pgmq
tag: 268398735f61008af099918a24b3fb57f9533ba3 tag: fcb7d4fb811e5b7239078b48268c469c8d28fdf9
tests: true tests: true
...@@ -83,9 +83,11 @@ library ...@@ -83,9 +83,11 @@ library
, haskell-pgmq >= 0.1.0.0 && < 0.2 , haskell-pgmq >= 0.1.0.0 && < 0.2
, hedis >= 0.15.2 && < 0.16 , hedis >= 0.15.2 && < 0.16
, mtl >= 2.2 && < 2.4 , mtl >= 2.2 && < 2.4
, postgresql-libpq >= 0.10 && < 0.11
, postgresql-simple >= 0.6 && < 0.8 , postgresql-simple >= 0.6 && < 0.8
, safe >= 0.3 && < 0.4 , safe >= 0.3 && < 0.4
, safe-exceptions >= 0.1.7 && < 0.2 , safe-exceptions >= 0.1.7 && < 0.2
, stm >= 2.5.3 && < 3
, text >= 1.2 && < 2.2 , text >= 1.2 && < 2.2
, time >= 1.10 && < 1.15 , time >= 1.10 && < 1.15
, units >= 2.4 && < 2.5 , units >= 2.4 && < 2.5
...@@ -204,4 +206,4 @@ test-suite test-integration ...@@ -204,4 +206,4 @@ test-suite test-integration
OverloadedStrings OverloadedStrings
RecordWildCards RecordWildCards
ghc-options: -threaded ghc-options: -threaded -fprof-auto
This diff is collapsed.
...@@ -19,7 +19,11 @@ module Async.Worker.Broker.PGMQ ...@@ -19,7 +19,11 @@ module Async.Worker.Broker.PGMQ
where where
import Async.Worker.Broker.Types (HasBroker(..), SerializableMessage) import Async.Worker.Broker.Types (HasBroker(..), SerializableMessage)
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (withMVar)
import Database.PostgreSQL.LibPQ qualified as LibPQ
import Database.PostgreSQL.Simple qualified as PSQL import Database.PostgreSQL.Simple qualified as PSQL
import Database.PostgreSQL.Simple.Internal qualified as PSQLInternal
import Database.PGMQ.Simple qualified as PGMQ import Database.PGMQ.Simple qualified as PGMQ
import Database.PGMQ.Types qualified as PGMQ import Database.PGMQ.Types qualified as PGMQ
...@@ -30,22 +34,30 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where ...@@ -30,22 +34,30 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where
data Broker PGMQBroker a = data Broker PGMQBroker a =
PGMQBroker' { PGMQBroker' {
conn :: PSQL.Connection conn :: PSQL.Connection
, defaultVt :: PGMQ.VisibilityTimeout
} }
data BrokerMessage PGMQBroker a = PGMQBM (PGMQ.Message a) data BrokerMessage PGMQBroker a = PGMQBM (PGMQ.Message a)
deriving (Show) deriving (Show)
data Message PGMQBroker a = PGMQM a data Message PGMQBroker a = PGMQM a
data MessageId PGMQBroker = PGMQMid Int data MessageId PGMQBroker = PGMQMid Int
deriving (Eq, Show) deriving (Eq, Show)
data BrokerInitParams PGMQBroker a = PGMQBrokerInitParams PSQL.ConnectInfo data BrokerInitParams PGMQBroker a = PGMQBrokerInitParams PSQL.ConnectInfo PGMQ.VisibilityTimeout
messageId (PGMQBM (PGMQ.Message { msgId })) = PGMQMid msgId messageId (PGMQBM (PGMQ.Message { msgId })) = PGMQMid msgId
getMessage (PGMQBM (PGMQ.Message { message })) = PGMQM message getMessage (PGMQBM (PGMQ.Message { message })) = PGMQM message
toMessage message = PGMQM message toMessage message = PGMQM message
toA (PGMQM message) = message toA (PGMQM message) = message
initBroker (PGMQBrokerInitParams connInfo) = do initBroker (PGMQBrokerInitParams connInfo defaultVt) = do
conn <- PSQL.connect connInfo conn <- PSQL.connect connInfo
-- PGMQ is quite verbose because of initialization. We can disable
-- notices
-- https://hackage.haskell.org/package/postgresql-simple-0.7.0.0/docs/src/Database.PostgreSQL.Simple.Internal.html#Connection
-- https://hackage.haskell.org/package/postgresql-libpq-0.10.1.0/docs/Database-PostgreSQL-LibPQ.html#g:13
-- https://www.postgresql.org/docs/current/libpq-notice-processing.html
withMVar (PSQLInternal.connectionHandle conn) $ \c -> do
LibPQ.disableNoticeReporting c
PGMQ.initialize conn PGMQ.initialize conn
pure $ PGMQBroker' { conn } pure $ PGMQBroker' { conn, defaultVt }
deinitBroker (PGMQBroker' { conn }) = PSQL.close conn deinitBroker (PGMQBroker' { conn }) = PSQL.close conn
createQueue (PGMQBroker' { conn }) queue = do createQueue (PGMQBroker' { conn }) queue = do
...@@ -54,17 +66,45 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where ...@@ -54,17 +66,45 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where
dropQueue (PGMQBroker' { conn }) queue = do dropQueue (PGMQBroker' { conn }) queue = do
PGMQ.dropQueue conn queue PGMQ.dropQueue conn queue
readMessageWaiting q@(PGMQBroker' { conn }) queue = loop readMessageWaiting q@(PGMQBroker' { conn, defaultVt }) queue = loop
where where
-- loop :: PGMQ.SerializableMessage a => IO (BrokerMessage PGMQBroker' a) -- loop :: PGMQ.SerializableMessage a => IO (BrokerMessage PGMQBroker' a)
loop = do loop = do
mMsg <- PGMQ.readMessageWithPoll conn queue 10 5 100 -- NOTE readMessageWithPoll is not thread-safe, i.e. the
-- blocking is outside of GHC (in PostgreSQL itself) and we
-- can't reliably use it in a highly concurrent situation.
-- mMsg <- PGMQ.readMessageWithPoll conn queue 10 5 100
mMsg <- PGMQ.readMessage conn queue defaultVt
case mMsg of case mMsg of
Nothing -> readMessageWaiting q queue Nothing -> do
Just msg -> return $ PGMQBM msg -- wait a bit, then retry
threadDelay (50 * 1000)
readMessageWaiting q queue
Just msg -> do
-- TODO! we want to set message visibility timeout so that other workers don't start this job
return $ PGMQBM msg
popMessageWaiting q@(PGMQBroker' { conn }) queue = loop
where
-- loop :: PGMQ.SerializableMessage a => IO (BrokerMessage PGMQBroker' a)
loop = do
-- mMsg <- PGMQ.readMessageWithPoll conn queue 10 5 100
mMsg <- PGMQ.popMessage conn queue
case mMsg of
Nothing -> do
-- wait a bit, then retry
threadDelay (50 * 1000)
popMessageWaiting q queue
Just msg -> do
-- TODO! we want to set message visibility timeout so that other workers don't start this job
return $ PGMQBM msg
setMessageTimeout (PGMQBroker' { conn }) queue (PGMQMid msgId) timeoutS =
PGMQ.setMessageVt conn queue msgId timeoutS
sendMessage (PGMQBroker' { conn }) queue (PGMQM message) = sendMessage (PGMQBroker' { conn }) queue (PGMQM message) =
PGMQ.sendMessage conn queue message 0 PGMQMid <$> PGMQ.sendMessage conn queue message 0
deleteMessage (PGMQBroker' { conn }) queue (PGMQMid msgId) = do deleteMessage (PGMQBroker' { conn }) queue (PGMQMid msgId) = do
PGMQ.deleteMessage conn queue msgId PGMQ.deleteMessage conn queue msgId
...@@ -73,10 +113,14 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where ...@@ -73,10 +113,14 @@ instance (SerializableMessage a, Show a) => HasBroker PGMQBroker a where
PGMQ.archiveMessage conn queue msgId PGMQ.archiveMessage conn queue msgId
getQueueSize (PGMQBroker' { conn }) queue = do getQueueSize (PGMQBroker' { conn }) queue = do
mMetrics <- PGMQ.getMetrics conn queue -- NOTE: pgmq.metrics is NOT a proper way to deal with messages
case mMetrics of -- that have vt in the future
Nothing -> return 0 -- (c.f. https://github.com/tembo-io/pgmq/issues/301)
Just (PGMQ.Metrics { queueLength }) -> return queueLength -- mMetrics <- PGMQ.getMetrics conn queue
-- case mMetrics of
-- Nothing -> return 0
-- Just (PGMQ.Metrics { queueLength }) -> return queueLength
PGMQ.queueAvailableLength conn queue
getArchivedMessage (PGMQBroker' { conn }) queue (PGMQMid msgId) = do getArchivedMessage (PGMQBroker' { conn }) queue (PGMQMid msgId) = do
mMsg <- PGMQ.readMessageFromArchive conn queue msgId mMsg <- PGMQ.readMessageFromArchive conn queue msgId
......
...@@ -12,10 +12,13 @@ https://redis.io/glossary/redis-queue/ ...@@ -12,10 +12,13 @@ https://redis.io/glossary/redis-queue/
The design is as follows: The design is as follows:
- for each queue we have an 'id counter' - for each queue we have an 'id counter'
- each queue is represented as a list of message ids - each queue is represented as a set of message ids
- each message is stored under unique key, derived from its id - each message is stored under unique key, derived from its id
- the above allows us to have an archive with messages - the above allows us to have an archive with messages
- deleting a message means removing it's unique key from Redis - deleting a message means removing it's unique key from Redis
The queue itself is a list, the archive is a set (so that we can use
SISMEMBER).
-} -}
...@@ -31,7 +34,7 @@ module Async.Worker.Broker.Redis ...@@ -31,7 +34,7 @@ module Async.Worker.Broker.Redis
where where
import Async.Worker.Broker.Types (HasBroker(..), Queue, SerializableMessage) import Async.Worker.Broker.Types (HasBroker(..), Queue, SerializableMessage)
import Control.Concurrent (threadDelay) -- import Control.Concurrent (threadDelay)
import Control.Monad (void) import Control.Monad (void)
import Data.Aeson qualified as Aeson import Data.Aeson qualified as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), (.:), (.=), withObject, object) import Data.Aeson (FromJSON(..), ToJSON(..), (.:), (.=), withObject, object)
...@@ -55,8 +58,6 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where ...@@ -55,8 +58,6 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where
deriving (Eq, Show) deriving (Eq, Show)
data BrokerInitParams RedisBroker a = RedisBrokerInitParams Redis.ConnectInfo data BrokerInitParams RedisBroker a = RedisBrokerInitParams Redis.ConnectInfo
-- We're using simple QUEUE so we don't care about message id as we
-- won't be deleting/archiving the messages
messageId (RedisBM (RedisWithMsgId { rmidId })) = RedisMid rmidId messageId (RedisBM (RedisWithMsgId { rmidId })) = RedisMid rmidId
getMessage (RedisBM (RedisWithMsgId { rmida })) = RedisM rmida getMessage (RedisBM (RedisWithMsgId { rmida })) = RedisM rmida
toMessage message = RedisM message toMessage message = RedisM message
...@@ -76,23 +77,45 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where ...@@ -76,23 +77,45 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where
let queueK = queueKey queue let queueK = queueKey queue
void $ Redis.runRedis conn $ Redis.del [queueK] void $ Redis.runRedis conn $ Redis.del [queueK]
readMessageWaiting b@(RedisBroker' { conn }) queue = loop -- TODO This is simplified
readMessageWaiting = popMessageWaiting
popMessageWaiting b@(RedisBroker' { conn }) queue = loop
where where
queueK = queueKey queue queueK = queueKey queue
loop = do loop = do
eMsgId <- Redis.runRedis conn $ Redis.spop queueK -- 0 means block indefinitely
case eMsgId of -- https://redis.io/docs/latest/commands/blpop/
eData <- Redis.runRedis conn $ Redis.blpop [queueK] 0
case eData of
Left _ -> undefined Left _ -> undefined
Right Nothing -> do Right Nothing -> undefined
threadDelay 100 Right (Just (_queueK, msgIdBS)) -> case bsToId msgIdBS of
readMessageWaiting b queue
Right (Just msgIdBS) -> case bsToId msgIdBS of
Nothing -> undefined Nothing -> undefined
Just msgId -> do Just msgId -> do
mMsg <- getRedisMessage b queue msgId mMsg <- getRedisMessage b queue msgId
case mMsg of maybe undefined return mMsg
Nothing -> undefined
Just msg -> return msg -- popMessageWaiting b@(RedisBroker' { conn }) queue = loop
-- where
-- queueK = queueKey queue
-- loop = do
-- eMsgId <- Redis.runRedis conn $ Redis.spop queueK
-- case eMsgId of
-- Left _ -> undefined
-- Right Nothing -> do
-- threadDelay (10*1000)
-- popMessageWaiting b queue
-- Right (Just msgIdBS) -> case bsToId msgIdBS of
-- Nothing -> undefined
-- Just msgId -> do
-- mMsg <- getRedisMessage b queue msgId
-- case mMsg of
-- Nothing -> undefined
-- Just msg -> return msg
setMessageTimeout _broker _queue _msgId _timeoutS =
pure ()
sendMessage b@(RedisBroker' { conn }) queue (RedisM message) = do sendMessage b@(RedisBroker' { conn }) queue (RedisM message) = do
mId <- nextId b queue mId <- nextId b queue
...@@ -104,34 +127,44 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where ...@@ -104,34 +127,44 @@ instance (SerializableMessage a, Show a) => HasBroker RedisBroker a where
let msgK = messageKey queue msgId let msgK = messageKey queue msgId
let queueK = queueKey queue let queueK = queueKey queue
void $ Redis.runRedis conn $ do void $ Redis.runRedis conn $ do
-- write the message itself under unique key
_ <- Redis.set msgK (BSL.toStrict $ Aeson.encode m) _ <- Redis.set msgK (BSL.toStrict $ Aeson.encode m)
Redis.sadd queueK [idToBS msgId] -- add message id to the list
-- Redis.sadd queueK [idToBS msgId]
Redis.lpush queueK [idToBS msgId]
return msgId
-- deleteMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do -- deleteMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do
deleteMessage (RedisBroker' { conn }) queue msgId = do deleteMessage (RedisBroker' { conn }) queue msgId = do
let queueK = queueKey queue let queueK = queueKey queue
void $ Redis.runRedis conn $ Redis.srem queueK [idToBS msgId]
let messageK = messageKey queue msgId let messageK = messageKey queue msgId
void $ Redis.runRedis conn $ Redis.del [messageK] -- void $ Redis.runRedis conn $ Redis.srem queueK [idToBS msgId]
void $ Redis.runRedis conn $ do
_ <- Redis.lrem queueK 1 (idToBS msgId)
Redis.del [messageK]
-- archiveMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do -- archiveMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do
archiveMessage (RedisBroker' { conn }) queue msgId = do archiveMessage (RedisBroker' { conn }) queue msgId = do
let queueK = queueKey queue let queueK = queueKey queue
let archiveK = archiveKey queue let archiveK = archiveKey queue
eMove <- Redis.runRedis conn $ Redis.smove queueK archiveK (idToBS msgId) void $ Redis.runRedis conn $ do
case eMove of _ <- Redis.lrem queueK 1 (idToBS msgId)
Left _ -> undefined Redis.sadd archiveK [idToBS msgId]
Right True -> return () -- eMove <- Redis.runRedis conn $ Redis.smove queueK archiveK (idToBS msgId)
Right False -> do -- case eMove of
-- OK so the queue might not have the id, we just add it to archive to make sure -- Left _ -> undefined
void $ Redis.runRedis conn $ Redis.sadd archiveK [idToBS msgId] -- Right True -> return ()
-- Right False -> do
-- -- OK so the queue might not have the id, we just add it to archive to make sure
-- void $ Redis.runRedis conn $ Redis.sadd archiveK [idToBS msgId]
getQueueSize (RedisBroker' { conn }) queue = do getQueueSize (RedisBroker' { conn }) queue = do
let queueK = queueKey queue let queueK = queueKey queue
eLen <- Redis.runRedis conn $ Redis.scard queueK -- eLen <- Redis.runRedis conn $ Redis.scard queueK
eLen <- Redis.runRedis conn $ Redis.llen queueK
case eLen of case eLen of
Right len -> return $ fromIntegral len Right len -> return $ fromIntegral len
Left _ -> return 0 Left _ -> undefined
getArchivedMessage b@(RedisBroker' { conn }) queue msgId = do getArchivedMessage b@(RedisBroker' { conn }) queue msgId = do
let archiveK = archiveKey queue let archiveK = archiveKey queue
...@@ -169,7 +202,7 @@ nextId (RedisBroker' { conn }) queue = do ...@@ -169,7 +202,7 @@ nextId (RedisBroker' { conn }) queue = do
-- | Key under which a message is stored -- | Key under which a message is stored
messageKey :: Queue -> MessageId RedisBroker -> BS.ByteString messageKey :: Queue -> MessageId RedisBroker -> BS.ByteString
messageKey queue (RedisMid msgId) = BS.pack $ beePrefix <> "queue-" <> queue <> "-message-" <> show msgId messageKey queue (RedisMid msgId) = queueKey queue <> BS.pack ("-message-" <> show msgId)
getRedisMessage :: FromJSON a getRedisMessage :: FromJSON a
=> Broker RedisBroker a => Broker RedisBroker a
......
...@@ -19,6 +19,7 @@ Broker typeclass definition. ...@@ -19,6 +19,7 @@ Broker typeclass definition.
module Async.Worker.Broker.Types module Async.Worker.Broker.Types
( Queue ( Queue
, TimeoutS
-- * Main broker typeclass -- * Main broker typeclass
-- $broker -- $broker
, HasBroker(..) , HasBroker(..)
...@@ -32,6 +33,7 @@ import Data.Typeable (Typeable) ...@@ -32,6 +33,7 @@ import Data.Typeable (Typeable)
type Queue = String type Queue = String
type TimeoutS = Int -- timeout for a message, in seconds
{- $broker {- $broker
...@@ -117,11 +119,23 @@ class ( ...@@ -117,11 +119,23 @@ class (
{-| Drop queue -} {-| Drop queue -}
dropQueue :: Broker b a -> Queue -> IO () dropQueue :: Broker b a -> Queue -> IO ()
{-| Read message, waiting for it if not present -} {-| Read message from queue, waiting for it if not present (this leaves
the message in queue, you need to use 'setMessageTimeout' to prevent
other workers from seeing this message). -}
readMessageWaiting :: Broker b a -> Queue -> IO (BrokerMessage b a) readMessageWaiting :: Broker b a -> Queue -> IO (BrokerMessage b a)
{-| Pop message from queue, waiting for it if not present -}
popMessageWaiting :: Broker b a -> Queue -> IO (BrokerMessage b a)
{-| We sometimes need a way to tell the broker that a message shouldn't
be visible for given amount of time (e.g. 'visibility timeout'
setting in PGMQ). The broker operates only on 'a' level and isn't
aware of 'Job' with its 'JobMetadata'. Hence, it's the worker's
responsibility to properly set timeout after message is read. -}
setMessageTimeout :: Broker b a -> Queue -> MessageId b -> TimeoutS -> IO ()
{-| Send message -} {-| Send message -}
sendMessage :: Broker b a -> Queue -> Message b a -> IO () sendMessage :: Broker b a -> Queue -> Message b a -> IO (MessageId b)
{-| Delete message -} {-| Delete message -}
deleteMessage :: Broker b a -> Queue -> MessageId b -> IO () deleteMessage :: Broker b a -> Queue -> MessageId b -> IO ()
......
...@@ -41,13 +41,12 @@ module Async.Worker.Types ...@@ -41,13 +41,12 @@ module Async.Worker.Types
-- * Other useful types and functions -- * Other useful types and functions
, HasWorkerBroker , HasWorkerBroker
, formatStr , formatStr
, JobTimeout(..) , JobTimeout(..) )
, JobException(..) )
where where
import Async.Worker.Broker.Types (Broker, BrokerMessage, HasBroker, Queue) import Async.Worker.Broker.Types (Broker, BrokerMessage, HasBroker, Queue)
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
import Control.Exception.Safe (Exception, SomeException) import Control.Exception.Safe (Exception)
import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), withObject, withText) import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), withObject, withText)
import Data.Text qualified as T import Data.Text qualified as T
import Data.Typeable (Typeable) import Data.Typeable (Typeable)
...@@ -156,7 +155,12 @@ data JobMetadata = ...@@ -156,7 +155,12 @@ data JobMetadata =
, timeout :: Timeout , timeout :: Timeout
-- | Read count so we know how many times this message -- | Read count so we know how many times this message
-- was processed -- was processed
, readCount :: ReadCount } , readCount :: ReadCount
-- | A worker might have processed a task and be
-- killed. If 'resendWhenWorkerKilled' is 'True', this
-- job will be resent to broker and picked up
-- later. Otherwise it will be discarded.
, resendWhenWorkerKilled :: Bool }
deriving (Eq, Show) deriving (Eq, Show)
instance ToJSON JobMetadata where instance ToJSON JobMetadata where
toJSON (JobMetadata { .. }) = toJSON (JobMetadata { .. }) =
...@@ -166,6 +170,7 @@ instance ToJSON JobMetadata where ...@@ -166,6 +170,7 @@ instance ToJSON JobMetadata where
, "tstrat" .= timeoutStrategy , "tstrat" .= timeoutStrategy
, "timeout" .= timeout , "timeout" .= timeout
, "readCount" .= readCount , "readCount" .= readCount
, "resendWhenWorkerKilled" .= resendWhenWorkerKilled
] ]
instance FromJSON JobMetadata where instance FromJSON JobMetadata where
parseJSON = withObject "JobMetadata" $ \o -> do parseJSON = withObject "JobMetadata" $ \o -> do
...@@ -174,6 +179,7 @@ instance FromJSON JobMetadata where ...@@ -174,6 +179,7 @@ instance FromJSON JobMetadata where
timeoutStrategy <- o .: "tstrat" timeoutStrategy <- o .: "tstrat"
timeout <- o .: "timeout" timeout <- o .: "timeout"
readCount <- o .: "readCount" readCount <- o .: "readCount"
resendWhenWorkerKilled <- o .: "resendWhenWorkerKilled"
return $ JobMetadata { .. } return $ JobMetadata { .. }
-- | For a typical 'Job' it's probably sane to just archive it no -- | For a typical 'Job' it's probably sane to just archive it no
...@@ -184,7 +190,8 @@ defaultMetadata = ...@@ -184,7 +190,8 @@ defaultMetadata =
, errorStrategy = ESArchive , errorStrategy = ESArchive
, timeoutStrategy = TSArchive , timeoutStrategy = TSArchive
, timeout = 10 , timeout = 10
, readCount = 0 } , readCount = 0
, resendWhenWorkerKilled = True }
-- | Worker 'Job' is 'a' (defining action to call via 'performAction') -- | Worker 'Job' is 'a' (defining action to call via 'performAction')
-- together with associated 'JobMetadata'. -- together with associated 'JobMetadata'.
...@@ -277,10 +284,3 @@ data JobTimeout b a = ...@@ -277,10 +284,3 @@ data JobTimeout b a =
, jtTimeout :: Timeout } , jtTimeout :: Timeout }
deriving instance (HasWorkerBroker b a) => Show (JobTimeout b a) deriving instance (HasWorkerBroker b a) => Show (JobTimeout b a)
instance (HasWorkerBroker b a) => Exception (JobTimeout b a) instance (HasWorkerBroker b a) => Exception (JobTimeout b a)
-- | An exception, thrown when job ends with error
data JobException b a =
JobException { jeBMessage :: BrokerMessage b (Job a)
, jeException :: SomeException }
deriving instance (HasWorkerBroker b a) => Show (JobException b a)
instance (HasWorkerBroker b a) => Exception (JobException b a)
...@@ -20,7 +20,8 @@ import Data.Aeson (ToJSON(..), FromJSON(..), withText) ...@@ -20,7 +20,8 @@ import Data.Aeson (ToJSON(..), FromJSON(..), withText)
import Data.Maybe (isJust) import Data.Maybe (isJust)
import Data.Text qualified as T import Data.Text qualified as T
import Test.Hspec import Test.Hspec
import Test.Integration.Utils (getPSQLEnvConnectInfo, getRedisEnvConnectInfo, randomQueueName, waitUntil) import Test.Integration.Utils (defaultPGMQVt, getPSQLEnvConnectInfo, getRedisEnvConnectInfo, randomQueueName, waitUntil)
import Test.RandomStrings (randomASCII, randomString, onlyAlphaNum)
data TestEnv b = data TestEnv b =
...@@ -72,28 +73,56 @@ brokerTests bInitParams = ...@@ -72,28 +73,56 @@ brokerTests bInitParams =
parallel $ around (withBroker bInitParams) $ describe "Broker tests" $ do parallel $ around (withBroker bInitParams) $ describe "Broker tests" $ do
it "can send and receive a message" $ \(TestEnv { broker, queue }) -> do it "can send and receive a message" $ \(TestEnv { broker, queue }) -> do
let msg = Message { text = "test" } let msg = Message { text = "test" }
BT.sendMessage broker queue (BT.toMessage msg) msgId <- BT.sendMessage broker queue (BT.toMessage msg)
msg2 <- BT.readMessageWaiting broker queue msg2 <- BT.readMessageWaiting broker queue
-- putStrLn $ "[messageId] " <> show (BT.messageId msg2) -- putStrLn $ "[messageId] " <> show (BT.messageId msg2)
msg `shouldBe` BT.toA (BT.getMessage msg2) msg `shouldBe` BT.toA (BT.getMessage msg2)
msgId `shouldBe` BT.messageId msg2
it "can send, archive and read message from archive" $ \(TestEnv { broker, queue }) -> do it "can send, archive and read message from archive" $ \(TestEnv { broker, queue }) -> do
let msg = Message { text = "test" } let msg = Message { text = "test" }
BT.sendMessage broker queue (BT.toMessage msg) msgId <- BT.sendMessage broker queue (BT.toMessage msg)
msg2 <- BT.readMessageWaiting broker queue msg2 <- BT.readMessageWaiting broker queue
let msgId = BT.messageId msg2 msgId `shouldBe` BT.messageId msg2
BT.archiveMessage broker queue msgId BT.archiveMessage broker queue msgId
putStrLn $ "Reading msg " <> show msgId <> " from archive queue " <> queue
-- It might take some time to archive a message so we wait a bit -- It might take some time to archive a message so we wait a bit
waitUntil (isJust <$> BT.getArchivedMessage broker queue msgId) 200 waitUntil (isJust <$> BT.getArchivedMessage broker queue msgId) 200
msgArchive <- BT.getArchivedMessage broker queue msgId msgArchive <- BT.getArchivedMessage broker queue msgId
let msgIdArchive = BT.messageId <$> msgArchive let msgIdArchive = BT.messageId <$> msgArchive
msgIdArchive `shouldBe` Just msgId msgIdArchive `shouldBe` Just msgId
it "returns correct message id when sending message to broker" $ \(TestEnv { broker, queue }) -> do
let iter = [1..20] :: [Int] -- number of steps
mapM_ (\_i -> do
-- Generate random strings and make sure that the
-- message ids we get from sendMessage match our data
text <- randomString (onlyAlphaNum randomASCII) 20
let msg = Message { text }
msgId <- BT.sendMessage broker queue (BT.toMessage msg)
bMsg <- BT.readMessageWaiting broker queue
msg `shouldBe` BT.toA (BT.getMessage bMsg)
msgId `shouldBe` BT.messageId bMsg
BT.deleteMessage broker queue msgId
) iter
it "preserves msgId when archiving a message" $ \(TestEnv { broker, queue }) -> do
let iter = [1..20] :: [Int] -- number of steps
mapM_ (\_i -> do
-- Generate random strings and make sure that the
-- message ids we get from sendMessage match our data
text <- randomString (onlyAlphaNum randomASCII) 20
let msg = Message { text }
msgId <- BT.sendMessage broker queue (BT.toMessage msg)
BT.archiveMessage broker queue msgId
msgArchive <- BT.getArchivedMessage broker queue msgId
Just msg `shouldBe` (BT.toA . BT.getMessage <$> msgArchive)
) iter
pgmqBrokerInitParams :: IO (BT.BrokerInitParams PGMQ.PGMQBroker Message) pgmqBrokerInitParams :: IO (BT.BrokerInitParams PGMQ.PGMQBroker Message)
pgmqBrokerInitParams = do pgmqBrokerInitParams = do
PGMQ.PGMQBrokerInitParams <$> getPSQLEnvConnectInfo conn <- getPSQLEnvConnectInfo
return $ PGMQ.PGMQBrokerInitParams conn defaultPGMQVt
redisBrokerInitParams :: IO (BT.BrokerInitParams Redis.RedisBroker Message) redisBrokerInitParams :: IO (BT.BrokerInitParams Redis.RedisBroker Message)
redisBrokerInitParams = do redisBrokerInitParams = do
......
module Test.Integration.Utils module Test.Integration.Utils
( getPSQLEnvConnectInfo ( defaultPGMQVt
, getPSQLEnvConnectInfo
, getRedisEnvConnectInfo , getRedisEnvConnectInfo
, randomQueueName , randomQueueName
, waitUntil , waitUntil
, waitUntilTVarEq , waitUntilTVarEq
, waitUntilTVarPred ) , waitUntilTVarPred
, waitUntilQueueSizeIs
, waitUntilQueueEmpty )
where where
import Async.Worker.Broker qualified as B import Async.Worker.Broker qualified as B
...@@ -20,6 +23,26 @@ import Test.Hspec (expectationFailure, shouldBe, shouldSatisfy, Expectation, Has ...@@ -20,6 +23,26 @@ import Test.Hspec (expectationFailure, shouldBe, shouldSatisfy, Expectation, Has
import Test.RandomStrings (randomASCII, randomString, onlyLower) import Test.RandomStrings (randomASCII, randomString, onlyLower)
-- | Timeout for 'wait' jobs, in ms.
newtype TimeoutMs = TimeoutMs Int
deriving (Eq, Show, Num, Integral, Real, Enum, Ord)
-- | Visibility timeout is a very important parameter for PGMQ. It is
-- mainly used when reading a job: it specifies for how many seconds
-- this job should be invisible for other workers. We need more tests
-- and setting this correctly, preferably in accordance with
-- 'Job.timeout'. Issue is that at the broker level we don't know
-- anything about 'Job'...
--
-- The lower the value, the more probable that some other worker will
-- pick up the same job at about the same time (before broker marks it
-- as invisible).
defaultPGMQVt :: Int
defaultPGMQVt = 1
-- | PSQL connect info that is fetched from env -- | PSQL connect info that is fetched from env
getPSQLEnvConnectInfo :: IO PSQL.ConnectInfo getPSQLEnvConnectInfo :: IO PSQL.ConnectInfo
getPSQLEnvConnectInfo = do getPSQLEnvConnectInfo = do
...@@ -46,10 +69,14 @@ randomQueueName prefix = do ...@@ -46,10 +69,14 @@ randomQueueName prefix = do
postfix <- randomString (onlyLower randomASCII) 10 postfix <- randomString (onlyLower randomASCII) 10
return $ prefix <> "_" <> postfix return $ prefix <> "_" <> postfix
waitThreadDelay :: Int
waitThreadDelay = 50 * 1000
-- | Given a predicate IO action, test it for given number of -- | Given a predicate IO action, test it for given number of
-- milliseconds or fail -- milliseconds or fail
waitUntil :: HasCallStack => IO Bool -> Int -> Expectation waitUntil :: HasCallStack => IO Bool -> TimeoutMs -> Expectation
waitUntil pred' timeoutMs = do waitUntil pred' (TimeoutMs timeoutMs) = do
_mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest _mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest
-- shortcut for testing mTimeout -- shortcut for testing mTimeout
p <- pred' p <- pred'
...@@ -61,12 +88,12 @@ waitUntil pred' timeoutMs = do ...@@ -61,12 +88,12 @@ waitUntil pred' timeoutMs = do
if p if p
then return () then return ()
else do else do
threadDelay 50 threadDelay waitThreadDelay
performTest performTest
-- | Similar to 'waitUntil' but specialized to 'TVar' equality checking -- | Similar to 'waitUntil' but specialized to 'TVar' equality checking
waitUntilTVarEq :: (HasCallStack, Show a, Eq a) => TVar a -> a -> Int -> Expectation waitUntilTVarEq :: (HasCallStack, Show a, Eq a) => TVar a -> a -> TimeoutMs -> Expectation
waitUntilTVarEq tvar expected timeoutMs = do waitUntilTVarEq tvar expected (TimeoutMs timeoutMs) = do
_mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest _mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest
-- shortcut for testing mTimeout -- shortcut for testing mTimeout
val <- readTVarIO tvar val <- readTVarIO tvar
...@@ -78,12 +105,12 @@ waitUntilTVarEq tvar expected timeoutMs = do ...@@ -78,12 +105,12 @@ waitUntilTVarEq tvar expected timeoutMs = do
if val == expected if val == expected
then return () then return ()
else do else do
threadDelay 50 threadDelay waitThreadDelay
performTest performTest
-- | Similar to 'waitUntilTVarEq' but with predicate checking -- | Similar to 'waitUntilTVarEq' but with predicate checking
waitUntilTVarPred :: (HasCallStack, Show a, Eq a) => TVar a -> (a -> Bool) -> Int -> Expectation waitUntilTVarPred :: (HasCallStack, Show a, Eq a) => TVar a -> (a -> Bool) -> TimeoutMs -> Expectation
waitUntilTVarPred tvar predicate timeoutMs = do waitUntilTVarPred tvar predicate (TimeoutMs timeoutMs) = do
_mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest _mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest
-- shortcut for testing mTimeout -- shortcut for testing mTimeout
val <- readTVarIO tvar val <- readTVarIO tvar
...@@ -95,5 +122,24 @@ waitUntilTVarPred tvar predicate timeoutMs = do ...@@ -95,5 +122,24 @@ waitUntilTVarPred tvar predicate timeoutMs = do
if predicate val if predicate val
then return () then return ()
else do else do
threadDelay 50 threadDelay waitThreadDelay
performTest performTest
waitUntilQueueSizeIs :: (B.HasBroker b a) => B.Broker b a -> B.Queue -> Int -> TimeoutMs -> Expectation
waitUntilQueueSizeIs b queue size (TimeoutMs timeoutMs) = do
_mTimeout <- Timeout.timeout (timeoutMs * 1000) performTest
qSize <- B.getQueueSize b queue
qSize `shouldBe` size
where
performTest = do
qSize <- B.getQueueSize b queue
if qSize == size
then return ()
else do
threadDelay waitThreadDelay
performTest
waitUntilQueueEmpty :: (B.HasBroker b a) => B.Broker b a -> B.Queue -> TimeoutMs -> Expectation
waitUntilQueueEmpty b queue timeoutMs = waitUntilQueueSizeIs b queue 0 timeoutMs
This diff is collapsed.
module Main where module Main where
import Test.Integration.Broker (brokerTests, pgmqBrokerInitParams, redisBrokerInitParams) import Test.Integration.Broker (brokerTests, pgmqBrokerInitParams, redisBrokerInitParams)
import Test.Integration.Worker (workerTests, pgmqWorkerBrokerInitParams, redisWorkerBrokerInitParams) import Test.Integration.Worker (workerTests, multiWorkerTests, pgmqWorkerBrokerInitParams, redisWorkerBrokerInitParams)
import Test.Tasty import Test.Tasty
import Test.Tasty.Hspec import Test.Tasty.Hspec
...@@ -13,18 +13,22 @@ main = do ...@@ -13,18 +13,22 @@ main = do
pgmqBrokerSpec <- testSpec "brokerTests (pgmq)" (brokerTests pgmqBInitParams) pgmqBrokerSpec <- testSpec "brokerTests (pgmq)" (brokerTests pgmqBInitParams)
pgmqWBInitParams <- pgmqWorkerBrokerInitParams pgmqWBInitParams <- pgmqWorkerBrokerInitParams
pgmqWorkerSpec <- testSpec "workerTests (pgmq)" (workerTests pgmqWBInitParams) pgmqWorkerSpec <- testSpec "workerTests (pgmq)" (workerTests pgmqWBInitParams)
pgmqMultiWorkerSpec <- testSpec "multiWorkerTests (pgmq)" (multiWorkerTests pgmqWBInitParams 5)
redisBInitParams <- redisBrokerInitParams redisBInitParams <- redisBrokerInitParams
redisBrokerSpec <- testSpec "brokerTests (redis)" (brokerTests redisBInitParams) redisBrokerSpec <- testSpec "brokerTests (redis)" (brokerTests redisBInitParams)
redisWBInitParams <- redisWorkerBrokerInitParams redisWBInitParams <- redisWorkerBrokerInitParams
redisWorkerSpec <- testSpec "workerTests (redis)" (workerTests redisWBInitParams) redisWorkerSpec <- testSpec "workerTests (redis)" (workerTests redisWBInitParams)
redisMultiWorkerSpec <- testSpec "multiWorkerTests (redis)" (multiWorkerTests redisWBInitParams 5)
defaultMain $ testGroup "integration tests" defaultMain $ testGroup "integration tests"
[ [
pgmqBrokerSpec pgmqBrokerSpec
, pgmqWorkerSpec , pgmqWorkerSpec
, pgmqMultiWorkerSpec
, redisBrokerSpec , redisBrokerSpec
, redisWorkerSpec , redisWorkerSpec
, redisMultiWorkerSpec
] ]
...@@ -45,6 +45,7 @@ instance QC.Arbitrary WT.JobMetadata where ...@@ -45,6 +45,7 @@ instance QC.Arbitrary WT.JobMetadata where
timeoutStrategy <- arbitrary timeoutStrategy <- arbitrary
timeout <- arbitrary timeout <- arbitrary
readCount <- arbitrary readCount <- arbitrary
resendWhenWorkerKilled <- arbitrary
return $ WT.JobMetadata { .. } return $ WT.JobMetadata { .. }
aesonPropJobMetadataTests = testGroup "Aeson WT.JobMetadata (de-)serialization tests" $ aesonPropJobMetadataTests = testGroup "Aeson WT.JobMetadata (de-)serialization tests" $
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment