packages feed

haskell-bee-redis-0.1.0.0: src/Async/Worker/Broker/Redis.hs

{-|
Module      : Async.Worker.Broker.Redis
Description : Redis broker functionality
Copyright   : (c) Gargantext, 2024-Present
License     : AGPL
Maintainer  : gargantext@iscpif.fr
Stability   : experimental
Portability : POSIX

Based on lists:
https://redis.io/glossary/redis-queue/

The design is as follows:
- for each queue we have an 'id counter'
- each queue is represented as a set of message ids
- each message is stored under unique key, derived from its id
- the above allows us to have an archive with messages
- 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).
-}


{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
    
module Async.Worker.Broker.Redis
  ( RedisBroker
  , BrokerInitParams(..)
  , RedisWithMsgId(..) )
where

import Async.Worker.Broker.Types (MessageBroker(..), Queue, SerializableMessage, TimeoutS(..), renderQueue)
-- import Control.Concurrent (threadDelay)
import Control.Monad (void)
import Data.Aeson qualified as Aeson
import Data.Aeson (FromJSON(..), ToJSON(..), (.:), (.=), withObject, object, withScientific)
import Data.ByteString.Char8 qualified as BSC
import Data.ByteString.Lazy qualified as BSL
import Data.Maybe (catMaybes, fromMaybe)
import Data.Scientific (floatingOrInteger)
import Data.UnixTime (getUnixTime, UnixTime(..))
import Database.Redis qualified as Redis
import Foreign.C.Types (CTime(..))
import Text.Read (readMaybe)


data RedisBroker

instance (SerializableMessage a, Show a) => MessageBroker RedisBroker a where
  data Broker RedisBroker a =
    RedisBroker' {
        conn :: Redis.Connection
      }
  data BrokerMessage RedisBroker a =
    RedisBM (RedisWithMsgId a)
    deriving (Show)
  data Message RedisBroker a = RedisM a
  data MessageId RedisBroker = RedisMid Int
    deriving (Eq, Show, Ord)
  data BrokerInitParams RedisBroker a = RedisBrokerInitParams Redis.ConnectInfo

  messageId (RedisBM (RedisWithMsgId { rmidId })) = RedisMid rmidId
  getMessage (RedisBM (RedisWithMsgId { rmida })) = RedisM rmida
  toMessage message = RedisM message
  toA (RedisM message) = message
  initBroker (RedisBrokerInitParams connInfo) = do
    conn <- Redis.checkedConnect connInfo
    pure $ RedisBroker' { conn }
  deinitBroker (RedisBroker' { conn }) = Redis.disconnect conn
  
  -- createQueue (RedisBroker' { conn }) queue = do
  createQueue _broker _queue = do
    -- No need to actually pre-create queues
    return ()

  -- dropQueue (RedisBroker' { conn }) queue = do
  dropQueue (RedisBroker' { conn }) queue = do
    let queueK = queueKey queue
    void $ Redis.runRedis conn $ Redis.del [queueK]

  -- TODO This is simplified
  readMessageWaiting = popMessageWaiting

  popMessageWaiting b@(RedisBroker' { conn }) queue = loop
    where
      queueK = queueKey queue
      loop = do
        -- 0 means block indefinitely
        -- https://redis.io/docs/latest/commands/blpop/
        eData <- Redis.runRedis conn $ Redis.blpop [queueK] 0
        case eData of
          Left _ -> undefined
          Right Nothing -> undefined
          Right (Just (_queueK, msgIdBS)) -> case bsToId msgIdBS of
            Nothing -> undefined
            Just msgId -> do
              mMsg <- getRedisMessage b queue msgId
              maybe undefined return mMsg

  -- 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 (RedisBroker' { conn }) queue msgId (TimeoutS timeoutS) = do
    ut <- getUnixTime
    void $ Redis.runRedis conn $ do
      let CTime t = utSeconds ut
      let ms = fromIntegral (utMicroSeconds ut) :: Integer
      Redis.hset queueK (idToBS msgId) (BSC.pack $ (show $ toInteger t + fromIntegral timeoutS) <> "." <> show ms)
    where
      queueK = messageTimeoutKey queue

  sendMessage b@(RedisBroker' { conn }) queue (RedisM message) = do
    mId <- nextId b queue
    case mId of
      Nothing -> undefined
      Just id' -> do
        let msgId = RedisMid id'
        let m = RedisWithMsgId { rmidId = id', rmida = message }
        let msgK = messageKey queue msgId
        let queueK = queueKey queue
        void $ Redis.runRedis conn $ do
          -- write the message itself under unique key
          _ <- Redis.set msgK (BSL.toStrict $ Aeson.encode m)
          -- add message id to the list
          -- Redis.sadd queueK [idToBS msgId]
          Redis.lpush queueK [idToBS msgId]
        return msgId

  -- | TODO Not implemented
  sendMessageDelayed b queue message _t = sendMessage b queue message

  -- deleteMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do
  deleteMessage (RedisBroker' { conn }) queue msgId = do
    let queueK = queueKey queue
    let messageK = messageKey queue msgId
    let timeoutK = messageTimeoutKey queue
    -- void $ Redis.runRedis conn $ Redis.srem queueK [idToBS msgId]
    void $ Redis.runRedis conn $ do
      void $ Redis.lrem queueK 1 (idToBS msgId)
      void $ Redis.del [messageK]
      Redis.hdel timeoutK [idToBS msgId]

  -- archiveMessage (RedisBroker' { conn }) queue (RedisMid msgId) = do
  archiveMessage (RedisBroker' { conn }) queue msgId = do
    let queueK = queueKey queue
    let archiveK = archiveKey queue
    let timeoutK = messageTimeoutKey queue
    void $ Redis.runRedis conn $ do
      void $ Redis.lrem queueK 1 (idToBS msgId)
      void $ Redis.sadd archiveK [idToBS msgId]
      Redis.hdel timeoutK [idToBS msgId]
    -- eMove <- Redis.runRedis conn $ Redis.smove queueK archiveK (idToBS msgId)
    -- case eMove of
    --   Left _ -> undefined
    --   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]

  -- TODO This is incorrect: we should include message timeout in this count
  -- getQueueSize (RedisBroker' { conn }) queue = do
  --   let queueK = queueKey queue
  --   -- eLen <- Redis.runRedis conn $ Redis.scard queueK
  --   eLen <- Redis.runRedis conn $ Redis.llen queueK
  --   case eLen of
  --     Right len -> return $ fromIntegral len
  --     Left _ -> undefined
  getQueueSize b queue = do
    msgIds <- listPendingMessageIds b queue
    pure $ length msgIds

  getArchivedMessage b@(RedisBroker' { conn }) queue msgId = do
    let archiveK = archiveKey queue
    eIsMember <- Redis.runRedis conn $ Redis.sismember archiveK (idToBS msgId)
    case eIsMember of
      Right True -> do
        getRedisMessage b queue msgId
      _ -> return Nothing

  listPendingMessageIds b@(RedisBroker' { conn }) queue = do
    let queueK = queueKey queue
    eMsgIds <- Redis.runRedis conn $ Redis.lrange queueK 0 (-1)
    case eMsgIds of
      Left _ -> return []
      Right msgIds -> do
        let msgIds' = catMaybes (bsToId <$> msgIds)
        ut <- getUnixTime
        timeouts <- mapM (getMessageTimeout b queue) msgIds'
        pure $ map fst $ filter (\(_msgId, ts) -> (fromMaybe ut ts) <= ut) $ zip msgIds' timeouts

  getMessageById b queue msgId = do
    getRedisMessage b queue msgId

getMessageTimeout :: Broker RedisBroker a -> Queue -> MessageId RedisBroker -> IO (Maybe UnixTime)
getMessageTimeout (RedisBroker' { conn }) queue msgId = do
  eData <- Redis.runRedis conn $ Redis.hget queueK (idToBS msgId)
  case eData of
    Left _ -> undefined
    Right Nothing -> pure Nothing
    Right (Just timeoutBs) -> do
      case BSC.break (== '.') timeoutBs of
        (s, ms) -> case (readMaybe $ BSC.unpack s, readMaybe $ BSC.unpack $ BSC.drop 1 ms) of
          (Just s', Just ms') -> pure $ Just $ UnixTime (CTime s') ms'
          _ -> pure Nothing
  where
    queueK = messageTimeoutKey queue

-- Helper functions for getting redis keys

-- | Redis counter is an 'Int', while sets can only store strings
idToBS :: MessageId RedisBroker -> BSC.ByteString
idToBS (RedisMid msgId) = BSL.toStrict $  Aeson.encode msgId

bsToId :: BSC.ByteString -> Maybe (MessageId RedisBroker)
bsToId bs = RedisMid <$> Aeson.decode (BSL.fromStrict bs)

-- | A global prefix used for all keys
beePrefix :: String
beePrefix = "bee-"

-- | Redis counter that returns message ids
idKey :: Queue -> BSC.ByteString
idKey queue = BSC.pack $ beePrefix <> "sequence-" <> renderQueue queue

nextId :: Broker RedisBroker a -> Queue -> IO (Maybe Int)
nextId (RedisBroker' { conn }) queue = do
  let key = idKey queue
  eId <- Redis.runRedis  conn $ Redis.incr key
  case eId of
    Right id' -> return (Just $ fromInteger id')
    _ -> return Nothing

-- | Key under which a message is stored
messageKey :: Queue -> MessageId RedisBroker -> BSC.ByteString
messageKey queue (RedisMid msgId) = queueKey queue <> BSC.pack ("-message-" <> show msgId)

-- | Key for storing the set of message ids in queue
queueKey :: Queue -> BSC.ByteString
queueKey queue = BSC.pack $ beePrefix <> "queue-" <> renderQueue queue

-- | Key for storing the set of message ids in archive
archiveKey :: Queue -> BSC.ByteString
archiveKey queue = BSC.pack $ beePrefix <> "archive-" <> renderQueue queue

-- | Key for storing message timeouts
messageTimeoutKey :: Queue -> BSC.ByteString
messageTimeoutKey queue = BSC.pack $ beePrefix <> "timeout-" <> renderQueue queue

getRedisMessage :: FromJSON a
                => Broker RedisBroker a
                -> Queue
                -> MessageId RedisBroker
                -> IO (Maybe (BrokerMessage RedisBroker a))
getRedisMessage (RedisBroker' { conn }) queue msgId = do
  let msgKey = messageKey queue msgId
  eMsg <- Redis.runRedis conn $ Redis.get msgKey
  case eMsg of
    Left _ -> return Nothing
    Right Nothing -> return Nothing
    Right (Just msg) ->
      case Aeson.decode (BSL.fromStrict msg) of
        Just dmsg -> return $ Just $ RedisBM dmsg
        Nothing -> return Nothing
    
-- | Helper datatype to store message with a unique id.
-- We fetch the id by using 'INCR'
-- https://redis.io/docs/latest/commands/incr/
data RedisWithMsgId a =
  RedisWithMsgId { rmida  :: a
                 , rmidId :: Int }
  deriving (Show, Eq)
instance FromJSON a => FromJSON (RedisWithMsgId a) where
  parseJSON = withObject "RedisWithMsgId" $ \o -> do
    rmida <- o .: "rmida"
    rmidId <- o .: "rmidId"
    return $ RedisWithMsgId { rmida, rmidId }
instance ToJSON a => ToJSON (RedisWithMsgId a) where
  toJSON (RedisWithMsgId { .. }) = toJSON $ object [
      "rmida" .= rmida
    , "rmidId" .= rmidId
    ]



instance ToJSON (MessageId RedisBroker) where
  toJSON (RedisMid i) = toJSON i
instance FromJSON (MessageId RedisBroker) where
  parseJSON = withScientific "RedisMid" $ \n ->
    case floatingOrInteger n of
      Right i -> pure $ RedisMid i
      Left (f :: Double) -> fail $ "Integer expected: " <> show f