packages feed

consumers-2.3.2.0: src/Database/PostgreSQL/Consumers/Config.hs

module Database.PostgreSQL.Consumers.Config
  ( Action (..)
  , Result (..)
  , ConsumerConfig (..)
  ) where

import Control.Exception (SomeException)
import Data.Time
import Database.PostgreSQL.PQTypes.FromRow
import Database.PostgreSQL.PQTypes.Interval
import Database.PostgreSQL.PQTypes.Notification
import Database.PostgreSQL.PQTypes.SQL
import Database.PostgreSQL.PQTypes.SQL.Raw

-- | Action to take after a job was processed.
data Action
  = MarkProcessed
  | RerunAfter Interval
  | RerunAt UTCTime
  | Remove
  deriving (Eq, Ord, Show)

-- | Result of processing a job.
data Result = Ok Action | Failed Action
  deriving (Eq, Ord, Show)

-- | Config of a consumer.
data ConsumerConfig m idx job = forall row. FromRow row => ConsumerConfig
  { ccJobsTable :: !(RawSQL ())
  -- ^ Name of the database table where jobs are stored. The table needs to have
  -- the following columns in order to be suitable for acting as a job queue:
  --
  -- * __id__ - represents ID of the job. Needs to be a primary key of a type
  -- convertible to text, not nullable.
  --
  -- * __run_at__ - represents the time at which the job will be
  -- processed. Needs to be nullable, of a type comparable with @now()@
  -- (TIMESTAMPTZ is recommended).
  --
  -- Note: a job with run_at set to NULL is never picked for processing. Useful
  -- for storing already processed/expired jobs for debugging purposes.
  --
  -- It's highly recommended to have an index on this column.
  --
  -- * __finished_at__ - represents the time at which job processing was
  -- finished. Needs to be nullable, of a type you can assign @now()@ to
  -- (TIMESTAMPTZ is recommended). NULL means that the job was either never
  -- processed or that it was started and failed at least once.
  --
  -- * __reserved_by__ - represents ID of the consumer that currently processes
  -- the job. Needs to be nullable, of the type corresponding to id in the table
  -- 'ccConsumersTable'. It's recommended (though not neccessary) to make it a
  -- foreign key referencing id in 'ccConsumersTable' with ON DELETE SET NULL.
  --
  -- * __attempts__ - represents number of job processing attempts made so
  -- far. Needs to be not nullable, of type INTEGER. Initial value of a fresh
  -- job should be 0, therefore it makes sense to make the column default to 0.
  , ccConsumersTable :: !(RawSQL ())
  -- ^ Name of a database table where registered consumers are stored. The table
  -- itself needs to have the following columns:
  --
  -- * __id__ - represents ID of a consumer. Needs to be a primary key of the
  -- type SERIAL or BIGSERIAL (recommended).
  --
  -- * __name__ - represents jobs table of the consumer. Needs to be not
  -- nullable, of type TEXT. Allows for tracking consumers of multiple queues
  -- with one table. Set to 'ccJobsTable'.
  --
  -- * __last_activity__ - represents the last registered activity of the
  -- consumer. It's updated periodically by all currently running consumers
  -- every 30 seconds to prove that they are indeed running. They also check for
  -- the registered consumers that didn't update their status for a minute. If
  -- any such consumers are found, they are presumed to be not working and all
  -- the jobs reserved by them are released. This prevents the situation where a
  -- consumer with reserved jobs silently fails (e.g. because of a hard crash)
  -- and these jobs stay locked forever, yet are never processed.
  , ccJobSelectors :: ![SQL]
  -- ^ Fields needed to be selected from the jobs table in order to assemble a
  -- job.
  , ccJobFetcher :: !(row -> job)
  -- ^ Function that transforms the list of fields into a job.
  , ccJobIndex :: !(job -> idx)
  -- ^ Selector for taking out job ID from the job object.
  , ccNotificationChannel :: !(Maybe Channel)
  -- ^ Notification channel used for listening for incoming jobs.  Whenever the
  -- consumer receives a notification, it checks the database for any pending
  -- jobs (@'run_at <= NOW()'@) and runs them all. If set to 'Nothing', no
  -- listening is performed and 'ccNotificationTimeout' should be set to a
  -- positive number, otherwise no jobs would be ever run.
  -- 'ccNotificationChannel' and 'ccNotificationTimeout' can be combined. The
  -- consumer will check for pending jobs either when notification is received
  -- or no notification is received for 'ccNotificationTimeout' microseconds
  -- since the last check.
  , ccNotificationTimeout :: !Int
  -- ^ Timeout of checking for any pending jobs (@'run_at <= NOW()'@), in
  -- microseconds. The consumer checks the database for any pending jobs after
  -- 'ccNotificationTimeout' microseconds since the last check was performed,
  -- runs them until all pending jobs are processed and after that, the cycle
  -- repeats. Note that even if 'ccNotificationChannel' is 'Just', you need to
  -- set 'ccNotificationTimeout' to a reasonable number if the jobs you process
  -- may fail and are retried later, as there is no way to signal with a
  -- notification that a job will need to be performed e.g. in 5
  -- minutes. However, if 'ccNotificationChannel' is 'Just' and jobs are never
  -- retried, you can set it to -1, then listening will never timeout. Otherwise
  -- it needs to be a positive number.
  , ccMaxRunningJobs :: !Int
  -- ^ Maximum amount of jobs that can be processed in parallel.
  , ccProcessJob :: !(job -> m Result)
  -- ^ Function that processes a job. It's recommended to process each job in a
  -- separate DB transaction, otherwise you'll have to remember to commit your
  -- changes to the database manually.
  , ccOnException :: !(SomeException -> job -> m Action)
  -- ^ Action taken if a job processing function throws an exception. For
  -- robustness it's best to ensure that it doesn't throw. If it does, the
  -- exception will be logged and the job in question postponed by a day.
  }