haskell-bee-0.1.0.0: src/Async/Worker/Types.hs
{-|
Module : Async.Worker.Types
Description : Types for the async worker
Copyright : (c) Gargantext, 2024-Present
License : AGPL
Maintainer : gargantext@iscpif.fr
Stability : experimental
Portability : POSIX
Types for worker.
-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
module Async.Worker.Types
(
-- * Main worker state
State(..)
-- * Job wrapped in metadata
, Job(..)
-- ** Job metadata
, JobMetadata(..)
, defaultMetadata
-- *** Strategies for handling finished, errored, timed-out jobs
, ArchiveStrategy(..)
, ErrorStrategy(..)
, TimeoutStrategy(..)
-- ** Job utility functions
, getJob
, jobTimeout
, Timeout
, runAction
-- ** Actions that the worker will perform
, PerformAction
-- * Events emitted during job lifetime
, WorkerJobEvent
, WorkerJobErrorEvent
, WorkerMJobEvent
-- * Other useful types and functions
, HasWorkerBroker
, formatStr
, JobTimeout(..) )
where
import Async.Worker.Broker.Types (Broker, BrokerMessage, MessageBroker, Queue)
import Control.Applicative ((<|>))
import Control.Exception.Safe (Exception, SomeException)
import Data.Aeson (FromJSON(..), ToJSON(..), object, (.=), (.:), withObject, withText)
import Data.Text qualified as T
import Data.Typeable (Typeable)
type ReadCount = Int
type Timeout = Int
-- | Strategy for archiving finished jobs
data ArchiveStrategy =
-- | Delete message when it's done
ASDelete
-- | Archive message when it's done
| ASArchive
deriving (Eq, Show)
instance ToJSON ArchiveStrategy where
toJSON ASDelete = toJSON ("ASDelete" :: String)
toJSON ASArchive = toJSON ("ASArchive" :: String)
instance FromJSON ArchiveStrategy where
parseJSON = withText "ArchiveStrategy" $ \s -> do
case s of
"ASDelete" -> pure ASDelete
"ASArchive" -> pure ASArchive
s' -> fail $ T.unpack s'
-- | Strategy for handling jobs with errors
data ErrorStrategy =
-- | Delete job when it threw an error
ESDelete
-- | Archive job when it threw an error
| ESArchive
-- | Try to repeat the job when an error ocurred (at most N
-- times), otherwise archive the job.
| ESRepeatNElseArchive Int
-- TODO Repeat N times
deriving (Eq, Show)
instance ToJSON ErrorStrategy where
toJSON ESDelete = toJSON ("ESDelete" :: String)
toJSON ESArchive = toJSON ("ESArchive" :: String)
toJSON (ESRepeatNElseArchive n) = toJSON $ object [ "ESRepeatNElseArchive" .= n ]
instance FromJSON ErrorStrategy where
parseJSON v = parseText v <|> parseESRepeatN v
where
parseText = withText "ErrorStrategy" $ \s -> do
case s of
"ESDelete" -> pure ESDelete
"ESArchive" -> pure ESArchive
s' -> fail $ T.unpack s'
parseESRepeatN = withObject "ESRepeatN" $ \o -> do
n <- o .: "ESRepeatNElseArchive"
pure $ ESRepeatNElseArchive n
-- | Strategy for handling job timeouts
data TimeoutStrategy =
-- | Delete job when it timed out
TSDelete
-- | Archive job when it timed out
| TSArchive
-- | Repeat job when it timed out (infinitely)
| TSRepeat
-- | Repeat job when it timed out (at most N times), otherwise archive it
| TSRepeatNElseArchive Int
-- | Repeat job when it timed out (at most N times), otherwise delete it
| TSRepeatNElseDelete Int
deriving (Eq, Show)
instance ToJSON TimeoutStrategy where
toJSON TSDelete = toJSON ("TSDelete" :: String)
toJSON TSArchive = toJSON ("TSArchive" :: String)
toJSON TSRepeat = toJSON ("TSRepeat" :: String)
toJSON (TSRepeatNElseArchive n) = toJSON $ object [ "TSRepeatNElseArchive" .= n ]
toJSON (TSRepeatNElseDelete n) = toJSON $ object [ "TSRepeatNElseDelete" .= n ]
instance FromJSON TimeoutStrategy where
parseJSON v = parseText v
<|> parseTSRepeatNElseArchive v
<|> parseTSRepeatNElseDelete v
where
-- | Parser for textual formats
parseText = withText "TimeoutStrategy (text)" $ \s -> do
case s of
"TSDelete" -> pure TSDelete
"TSArchive" -> pure TSArchive
"TSRepeat" -> pure TSRepeat
s' -> fail $ T.unpack s'
-- | Parser for 'TSRepeatN' object
parseTSRepeatNElseArchive = withObject "TimeoutStrategy (TSRepeatNElseArchive)" $ \o -> do
n <- o .: "TSRepeatNElseArchive"
pure $ TSRepeatNElseArchive n
parseTSRepeatNElseDelete = withObject "TimeoutStrategy (TSRepeatNElseDelete)" $ \o -> do
n <- o .: "TSRepeatNElseDelete"
pure $ TSRepeatNElseDelete n
-- | Metadata associated with a job.
data JobMetadata =
JobMetadata {
-- | What to do after job is finished successfully
archiveStrategy :: ArchiveStrategy
-- | What to do when a job ends with error
, errorStrategy :: ErrorStrategy
-- | What to do when a job ends with timeout
, timeoutStrategy :: TimeoutStrategy
-- | Time after which the job is considered to time-out
-- (in seconds).
, timeout :: Timeout
-- | Read count so we know how many times this message
-- was processed
, 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)
instance ToJSON JobMetadata where
toJSON (JobMetadata { .. }) =
toJSON $ object [
"astrat" .= archiveStrategy
, "estrat" .= errorStrategy
, "tstrat" .= timeoutStrategy
, "timeout" .= timeout
, "readCount" .= readCount
, "resendWhenWorkerKilled" .= resendWhenWorkerKilled
]
instance FromJSON JobMetadata where
parseJSON = withObject "JobMetadata" $ \o -> do
archiveStrategy <- o .: "astrat"
errorStrategy <- o .: "estrat"
timeoutStrategy <- o .: "tstrat"
timeout <- o .: "timeout"
readCount <- o .: "readCount"
resendWhenWorkerKilled <- o .: "resendWhenWorkerKilled"
return $ JobMetadata { .. }
-- | For a typical 'Job' it's probably sane to just archive it no
-- matter how it finished.
defaultMetadata :: JobMetadata
defaultMetadata =
JobMetadata { archiveStrategy = ASArchive
, errorStrategy = ESArchive
, timeoutStrategy = TSArchive
, timeout = 10
, readCount = 0
, resendWhenWorkerKilled = True }
-- | Worker 'Job' is 'a' (defining action to call via 'performAction')
-- together with associated 'JobMetadata'.
data Job a =
Job { job :: a
, metadata :: JobMetadata }
deriving (Eq, Show)
-- | Given worker 'Job' 'a', return the 'a' part
getJob :: Job a -> a
getJob (Job { job }) = job
instance ToJSON a => ToJSON (Job a) where
toJSON (Job { .. }) =
toJSON $ object [
"metadata" .= metadata
, "job" .= job
]
instance FromJSON a => FromJSON (Job a) where
parseJSON = withObject "Job" $ \o -> do
metadata <- o .: "metadata"
job <- o .: "job"
return $ Job { .. }
-- | For a given 'Job', return it's 'timeout' value from
-- 'JobMetadata'.
jobTimeout :: Job a -> Timeout
jobTimeout (Job { metadata }) = timeout metadata
-- | Main state for a running worker ('b' is 'Broker', 'a' is the
-- underlying message).
--
-- 'a' is the underlying message specification and is
-- implementation-dependent, e.g. can be of JSON form
--
-- @
-- {"function": ..., "arguments": [...]}
-- @
--
-- 'Job' 'a' is worker's wrapper around that message with metadata
-- (corresponds to broker 'Async.Worker.Broker.Types.Message' 'b'
-- 'a'). 'BrokerMessage' 'b 'a', on the other hand, is what we get
-- when the broker reads that message).
--
-- Note that our underlying 'Broker' handles messages of type 'Job'
-- 'a'.
data State b a =
State { broker :: Broker b (Job a)
-- | Queue associated with this worker. If you want to support
-- more queues, spawn more workers.
, queueName :: Queue
-- | Name of this worker (useful for debugging).
, name :: String
-- | Actions that will be performed when message 'a' arrives.
, performAction :: PerformAction b a
-- | Event emitted after job was received from broker
, onMessageReceived :: WorkerJobEvent b a
-- | Event emitted after job was finished successfully
, onJobFinish :: WorkerJobEvent b a
-- | Event emitted after job timed out
, onJobTimeout :: WorkerJobEvent b a
-- | Event emitted after job ended with error
, onJobError :: WorkerJobErrorEvent b a
-- | Event emitted when worker is safely killed (don't overuse it)
, onWorkerKilledSafely :: WorkerMJobEvent b a }
-- | Helper function to call an action for given worker, for a
-- 'BrokerMessage'.
runAction :: State b a -> BrokerMessage b (Job a) -> IO ()
runAction state brokerMessage = (performAction state) state brokerMessage
type WorkerJobEvent b a = Maybe (State b a -> BrokerMessage b (Job a) -> IO ())
type WorkerJobErrorEvent b a = Maybe (State b a -> BrokerMessage b (Job a) -> SomeException -> IO ())
type WorkerMJobEvent b a = Maybe (State b a -> Maybe (BrokerMessage b (Job a)) -> IO ())
-- | Callback definition (what to execute when a message arrives)
type PerformAction b a =
State b a -> BrokerMessage b (Job a) -> IO ()
-- | /TODO/ 'Show' 'a' could be removed. Any logging can be done as
-- part of 'on' events in 'State'.
type HasWorkerBroker b a = ( MessageBroker b (Job a), Typeable a, Typeable b, Show a )
-- | Helper function to format a string with worker name (for logging)
formatStr :: State b a -> String -> String
formatStr (State { name }) msg =
"[" <> name <> "] " <> msg
-- | An exception, thrown when job times out
data JobTimeout b a =
JobTimeout { jtBMessage :: BrokerMessage b (Job a)
, jtTimeout :: Timeout }
deriving instance (Show (BrokerMessage b (Job a))) => Show (JobTimeout b a)
instance (Show (BrokerMessage b (Job a)), Typeable a, Typeable b) => Exception (JobTimeout b a)