module Utils.Sql
  ( setSerializable
  ) where

import ClassyPrelude.Yesod

import Database.PostgreSQL.Simple (SqlError(SqlError), sqlErrorHint)
import Control.Monad.Catch (MonadMask)

import Database.Persist.Sql
import Database.Persist.Sql.Raw.QQ

import Control.Retry

import Control.Lens ((&))


setSerializable :: forall m a. (MonadLogger m, MonadMask m, MonadIO m) => ReaderT SqlBackend m a -> ReaderT SqlBackend m a
setSerializable act = recovering policy [logRetries suggestRetry logRetry] act'
  where
    policy :: RetryPolicyM (ReaderT SqlBackend m)
    policy = fullJitterBackoff 1e3 & limitRetriesByCumulativeDelay 10e6

    suggestRetry :: SqlError -> ReaderT SqlBackend m Bool
    suggestRetry SqlError{sqlErrorHint} = return $ "The transaction might succeed if retried." `isInfixOf` sqlErrorHint
  
    logRetry :: Bool -- ^ Will retry
             -> SqlError
             -> RetryStatus
             -> ReaderT SqlBackend m ()
    logRetry shouldRetry err status = $logDebugS "SQL" . pack $ defaultLogMsg shouldRetry err status

    act' :: RetryStatus -> ReaderT SqlBackend m a
    act' RetryStatus{..}
      | rsIterNumber == 0 = [executeQQ|SET TRANSACTION ISOLATION LEVEL SERIALIZABLE|] *> act
      | otherwise         = transactionUndoWithIsolation Serializable *> act
