haskell-bee-pgmq-0.1.0.0: src/Async/Worker/Broker/PGMQ.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-|
Module : Async.Worker.Broker.PGMQ
Description : PGMQ broker implementation
Copyright : (c) Gargantext, 2024-Present
License : AGPL
Maintainer : gargantext@iscpif.fr
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module Async.Worker.Broker.PGMQ
( PGMQBroker
, BrokerInitParams(..) )
where
import Async.Worker.Broker.Types (MessageBroker(..), SerializableMessage, renderQueue, TimeoutS(..))
import Data.ByteString qualified as BS
import Control.Concurrent (threadDelay)
import Control.Concurrent.MVar (withMVar)
import Control.DeepSeq
import Data.Aeson (FromJSON(..), ToJSON(..), withScientific)
import Data.Scientific (floatingOrInteger)
import Database.PostgreSQL.LibPQ qualified as LibPQ
import Database.PostgreSQL.Simple qualified as PSQL
import Database.PostgreSQL.Simple.Internal qualified as PSQLInternal
import Database.PGMQ qualified as PGMQ
import GHC.Generics
data PGMQBroker
instance (SerializableMessage a, Show a) => MessageBroker PGMQBroker a where
data Broker PGMQBroker a =
PGMQBroker' {
conn :: PSQL.Connection
-- | This is the default visibility timeout.
-- It is important to set this to something > 0, otherwise 2
-- workers might get the same message at the same time.
-- This value doesn't need to be large, because later you can specify
-- 'timeout' setting in job metadata.
-- See: https://tembo.io/pgmq/#visibility-timeout-vt
, defaultVt :: PGMQ.VisibilityTimeout
}
newtype BrokerMessage PGMQBroker a = PGMQBM (PGMQ.Message a)
deriving (Show)
newtype Message PGMQBroker a = PGMQM a
newtype MessageId PGMQBroker = PGMQMid Int
deriving (Eq, Show, Ord, Generic)
data BrokerInitParams PGMQBroker a =
PGMQBrokerInitParams PSQL.ConnectInfo PGMQ.VisibilityTimeout
| PGMQBrokerInitConnStr BS.ByteString PGMQ.VisibilityTimeout
messageId (PGMQBM (PGMQ.Message { msgId })) = PGMQMid msgId
getMessage (PGMQBM (PGMQ.Message { message })) = PGMQM message
toMessage message = PGMQM message
toA (PGMQM message) = message
initBroker (PGMQBrokerInitParams connInfo defaultVt) = do
initBroker (PGMQBrokerInitConnStr (PSQL.postgreSQLConnectionString connInfo) defaultVt)
initBroker (PGMQBrokerInitConnStr connStr defaultVt) = do
conn <- PSQL.connectPostgreSQL connStr
-- 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
pure $ PGMQBroker' { conn, defaultVt }
deinitBroker (PGMQBroker' { conn }) = PSQL.close conn
createQueue (PGMQBroker' { conn }) (renderQueue -> queue) = do
PGMQ.createQueue conn queue
dropQueue (PGMQBroker' { conn }) (renderQueue -> queue) = do
PGMQ.dropQueue conn queue
readMessageWaiting (PGMQBroker' { conn, defaultVt }) queue = loop
where
-- loop :: PGMQ.SerializableMessage a => IO (BrokerMessage PGMQBroker' a)
loop = do
-- 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.
-- NOTE! This sets message visibility timeout so that other
-- workers don't start this job at the same time!
mMsg <- PGMQ.readMessage conn (renderQueue queue) defaultVt
case mMsg of
Nothing -> do
-- wait a bit, then retry
threadDelay (150 * 1000)
loop
Just msg -> do
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 (renderQueue 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 }) (renderQueue -> queue) (PGMQMid msgId) (TimeoutS timeoutS) =
PGMQ.setMessageVt conn queue msgId timeoutS
sendMessage b queue message = do
sendMessageDelayed b queue message (TimeoutS 0)
sendMessageDelayed (PGMQBroker' { conn }) (renderQueue -> queue) (PGMQM message) (TimeoutS t) = do
PGMQMid <$> PGMQ.safeSendMessage conn queue message t
deleteMessage (PGMQBroker' { conn }) (renderQueue -> queue) (PGMQMid msgId) = do
PGMQ.deleteMessage conn queue msgId
archiveMessage (PGMQBroker' { conn }) (renderQueue -> queue) (PGMQMid msgId) = do
PGMQ.archiveMessage conn queue msgId
getQueueSize (PGMQBroker' { conn }) (renderQueue -> queue) = do
-- NOTE: pgmq.metrics is NOT a proper way to deal with messages
-- that have vt in the future
-- (c.f. https://github.com/tembo-io/pgmq/issues/301)
-- mMetrics <- PGMQ.getMetrics conn queue
-- case mMetrics of
-- Nothing -> return 0
-- Just (PGMQ.Metrics { queueLength }) -> return queueLength
PGMQ.queueAvailableLength conn queue
getArchivedMessage (PGMQBroker' { conn }) (renderQueue -> queue) (PGMQMid msgId) = do
mMsg <- PGMQ.readMessageFromArchive conn queue msgId
pure $ PGMQBM <$> mMsg
listPendingMessageIds (PGMQBroker' { conn }) (renderQueue -> queue) = do
msgIds <- PGMQ.queueAvailableIds conn queue
pure $ PGMQMid <$> msgIds
getMessageById (PGMQBroker' { conn }) (renderQueue -> queue) (PGMQMid msgId) = do
mMsg <- PGMQ.readMessageById conn queue msgId
pure $ PGMQBM <$> mMsg
deriving anyclass instance NFData (MessageId PGMQBroker)
instance ToJSON (MessageId PGMQBroker) where
toJSON (PGMQMid i) = toJSON i
instance FromJSON (MessageId PGMQBroker) where
parseJSON = withScientific "PGMQMid" $ \n ->
case floatingOrInteger n of
Right i -> pure $ PGMQMid i
Left (f :: Double) -> fail $ "Integer expected: " <> show f