packages feed

haskell-bee-stm-0.1.0.0: src/Async/Worker/Broker/STM.hs

{-|
Module      : Async.Worker.Broker.STM
Description : Simple, STM-based broker, mostly used for tests
Copyright   : (c) Gargantext, 2024-Present
License     : AGPL
Maintainer  : gargantext@iscpif.fr
Stability   : experimental
Portability : POSIX

-}


{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
    
module Async.Worker.Broker.STM
  ( STMBroker
  , BrokerInitParams(..)
  , STMWithMsgId(..) )
where

import Async.Worker.Broker.Types (MessageBroker(..), Queue, TimeoutS(..))
import Control.Concurrent (threadDelay)
import Control.Monad.STM (atomically)
import Control.Concurrent.STM.TVar
import Data.Aeson (FromJSON(..), ToJSON(..), (.:), (.=), withObject, object, withScientific)
import Data.Map.Strict qualified as Map
import Data.Maybe (fromMaybe)
import Data.Scientific (floatingOrInteger)
import Data.UnixTime
import Safe (headMay)


data STMBroker

instance (Show a) => MessageBroker STMBroker a where
  data Broker STMBroker a =
    STMBroker' {
        stmMap :: TVar (Map.Map Queue (Map.Map Int (STMWithMsgId a)))
      , archiveMap :: TVar (Map.Map Queue (Map.Map Int (STMWithMsgId a)))
      , counter :: TVar Int  -- used to issue new ids
      }
  data BrokerMessage STMBroker a =
    STMBM (STMWithMsgId a)
    deriving (Show)
  data Message STMBroker a = STMM a
  data MessageId STMBroker = STMMid Int
    deriving (Eq, Show, Ord)
  data BrokerInitParams STMBroker a =
    STMBrokerInitParams { archiveMap :: TVar (Map.Map Queue (Map.Map Int (STMWithMsgId a)))
                        , stmMap :: TVar (Map.Map Queue (Map.Map Int (STMWithMsgId a))) }

  messageId (STMBM (STMWithMsgId { stmidId })) = STMMid stmidId
  getMessage (STMBM (STMWithMsgId { stmida })) = STMM stmida
  toMessage message = STMM message
  toA (STMM message) = message
  initBroker (STMBrokerInitParams { .. }) = do
    counter <- newTVarIO 0
    pure $ STMBroker' { counter, .. }
  deinitBroker (STMBroker' { }) = pure ()
  
  createQueue (STMBroker' { stmMap }) queue = do
    atomically $ modifyTVar stmMap (Map.alter f queue)
    where
      f Nothing   = Just Map.empty
      f (Just qs) = Just qs

  dropQueue (STMBroker' { stmMap }) queue = do
    atomically $ modifyTVar stmMap (Map.alter (const Nothing) queue)
    

  readMessageWaiting = popMessageWaiting
  
  popMessageWaiting (STMBroker' { stmMap }) queue = loop
    where
      loop = do
        ut <- getUnixTime
        mMsg <- atomically $ do
          map' <- readTVar stmMap
          case Map.lookup queue map' of
            Nothing -> pure Nothing
            Just qm -> do
              let qm' = Map.filter (\s -> stmidInvisibleUntil s <= ut) qm
              case snd <$> (headMay $ Map.toList qm') of
                Nothing -> pure Nothing
                Just msg' -> do
                  -- modifyTVar stmMap $ Map.insert queue (Map.insert (stmidId msg') (msg' { stmidIsAvailable = False}) qm)
                  pure $ Just msg'
        case mMsg of
          Just msg -> pure $ STMBM msg
          Nothing -> do
            threadDelay 10_000
            loop

  setMessageTimeout (STMBroker' { stmMap }) queue (STMMid msgId) timeoutS = do
    atomically $ do
      let dt = secondsToUnixDiffTime $ _TimeoutS timeoutS
      let f s = Just $ s { stmidInvisibleUntil = addUnixDiffTime (stmidInvisibleUntil s) dt }
      modifyTVar stmMap $ Map.update (\qm -> Just $ Map.update f msgId qm) queue
    
  -- setMessageTimeout (STMBroker' { stmMap }) queue msgId timeoutS = do
  --   ut <- getUnixTime
  --   atomically $ do
  --     map' <- readTVar stmMap
  --     case Map.lookup queue map' of
  --       Nothing -> undefined
  --       Just qs -> do
  --         case filter (\(STMWithMsgId { stmidId }) -> stmidId == msgId) qs of
  --           Nothing -> undefined
  --           Just q -> do

  sendMessage b queue message = sendMessageDelayed b queue message (TimeoutS 0)

  sendMessageDelayed (STMBroker' { stmMap, counter }) queue (STMM message) (TimeoutS t) = do
    id' <- atomically $ do
      modifyTVar counter (+1)
      readTVar counter
    let msgId = STMMid id'
    ut' <- getUnixTime
    let ut = ut' { utSeconds = utSeconds ut' + fromIntegral t }
    let m = STMWithMsgId { stmidId = id', stmida = message, stmidInvisibleUntil = ut }
    let f x = case x of
          Nothing -> Just $ Map.singleton id' m
          Just qm -> Just $ Map.insert id' m qm
    atomically $ do
      modifyTVar stmMap (Map.alter f queue)
      
    return msgId

  deleteMessage (STMBroker' { stmMap }) queue (STMMid msgId) = do
    atomically $ do
      modifyTVar stmMap $ Map.adjust (Map.delete msgId) queue
          -- modifyTVar stmMap $ Map.insert queue (filter (\(STMWithMsgId { stmidId }) -> msgId /= stmidId) qs)

  archiveMessage (STMBroker' { archiveMap, stmMap }) queue (STMMid msgId) = do
    atomically $ do
      map' <- readTVar stmMap
      case Map.lookup queue map' of
        Nothing -> pure ()
        Just qm -> do
          let el' = Map.lookup msgId qm
          -- let el' = headMay $ filter (\(STMWithMsgId { stmidId }) -> msgId == stmidId) qs
          modifyTVar stmMap $ Map.adjust (Map.delete msgId) queue
          -- modifyTVar stmMap $ Map.insert queue (filter (\(STMWithMsgId { stmidId }) -> msgId /= stmidId) qs)
          let f = case el' of
                Nothing -> id
                Just el'' -> \am -> Just (Map.insert msgId el'' $ fromMaybe Map.empty am)
          modifyTVar archiveMap $ Map.alter f queue

  getQueueSize broker queue = do
    length <$> filterAvailableMessages broker queue

  getArchivedMessage (STMBroker' { archiveMap }) queue (STMMid msgId) = do
    -- m' <- readTVarIO archiveMap
    -- putStrLn $ "[getArchivedMessage] m': " <> show m'
    atomically $ do
      map' <- readTVar archiveMap
      case Map.lookup queue map' of
        Nothing -> pure Nothing
        Just qm ->
          pure $ STMBM <$> (Map.lookup msgId qm)

  listPendingMessageIds broker queue = do
    msgIds <- Map.keys <$> filterAvailableMessages broker queue
    pure $ STMMid <$> msgIds

  getMessageById (STMBroker' { stmMap }) queue (STMMid msgId) = do
    atomically $ do
      map' <- readTVar stmMap
      case Map.lookup queue map' of
        Nothing -> pure Nothing
        Just qm ->
          pure $ STMBM <$> (Map.lookup msgId qm)


filterAvailableMessages :: Broker STMBroker a -> Queue -> IO (Map.Map Int (STMWithMsgId a))
filterAvailableMessages (STMBroker' { stmMap }) queue = do
    um <- getUnixTime
    atomically $ do
      map' <- readTVar stmMap
      pure $ Map.filter (\s -> stmidInvisibleUntil s <= um) $ fromMaybe Map.empty $ Map.lookup queue map'
      
-- | Helper datatype to store message with a unique id.
-- We fetch the id by using 'INCR'
-- https://redis.io/docs/latest/commands/incr/
data STMWithMsgId a =
  STMWithMsgId { stmida  :: a
               , stmidInvisibleUntil :: UnixTime
               , stmidId :: Int }
  deriving (Show, Eq)
instance FromJSON a => FromJSON (STMWithMsgId a) where
  parseJSON = withObject "STMWithMsgId" $ \o -> do
    stmida <- o .: "stmida"
    -- timeout handling
    stmidInvisibleUntilSec <- o .: "stmidInvisibleUntilSec"
    stmidInvisibleUntilMs <- o .: "stmidInvisibleUntilMs"
    let stmidInvisibleUntil = UnixTime { utSeconds = stmidInvisibleUntilSec
                                       , utMicroSeconds = stmidInvisibleUntilMs }
    stmidId <- o .: "stmidId"
    return $ STMWithMsgId { stmida, stmidInvisibleUntil, stmidId }
instance ToJSON a => ToJSON (STMWithMsgId a) where
  toJSON (STMWithMsgId { .. }) = toJSON $ object [
      "stmida" .= stmida
    , "stmidInvisibleUntilSec" .= utSeconds stmidInvisibleUntil
    , "stmidInvisibleUntilMs" .= utMicroSeconds stmidInvisibleUntil
    , "stmidId" .= stmidId
    ]



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