feat(db): automatic retry of database transactions upon system error

BREAKING CHANGE: transactions need to be retryable, now
This commit is contained in:
Gregor Kleen 2020-07-16 15:02:10 +02:00
parent 1926917dd7
commit e7a5162ec9
2 changed files with 38 additions and 3 deletions

View File

@ -107,6 +107,9 @@ import Web.Cookie
import Yesod.Core.Types (GHState(..), HandlerData(..), HandlerContents, RunHandlerEnv(rheSite, rheChild)) import Yesod.Core.Types (GHState(..), HandlerData(..), HandlerContents, RunHandlerEnv(rheSite, rheChild))
import Database.Persist.Sql (transactionUndo, SqlReadBackend(..)) import Database.Persist.Sql (transactionUndo, SqlReadBackend(..))
import qualified Control.Retry as Retry
import GHC.IO.Exception (IOErrorType(OtherError))
-- | Convenient Type Synonyms: -- | Convenient Type Synonyms:
type DB = YesodDB UniWorX type DB = YesodDB UniWorX
type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, Widget) type Form x = Html -> MForm (HandlerFor UniWorX) (FormResult x, Widget)
@ -4564,10 +4567,37 @@ routeNormalizers =
return newRoute return newRoute
runSqlPoolRetry :: forall m a backend.
( MonadUnliftIO m, BackendCompatible SqlBackend backend
, MonadLogger m, MonadMask m
)
=> ReaderT backend m a
-> Pool backend
-> m a
runSqlPoolRetry action pool = do
let policy = Retry.fullJitterBackoff 1e3 & Retry.limitRetriesByCumulativeDelay 10e6
handlers = Retry.skipAsyncExceptions `snoc` Retry.logRetries suggestRetry logRetry
where suggestRetry :: IOException -> m Bool
suggestRetry ioExc = return $
ioeGetErrorType ioExc == OtherError
&& ioeGetLocation ioExc == "libpq"
logRetry :: forall e.
Exception e
=> Bool -- ^ Will retry
-> e
-> Retry.RetryStatus
-> m ()
logRetry shouldRetry@False err status = $logErrorS "runSqlPoolRetry" . pack $ Retry.defaultLogMsg shouldRetry err status
logRetry shouldRetry@True err status = $logWarnS "runSqlPoolRetry" . pack $ Retry.defaultLogMsg shouldRetry err status
Retry.recovering policy handlers $ \Retry.RetryStatus{..} -> do
$logDebugS "runSqlPoolRetry" $ "rsIterNumber = " <> tshow rsIterNumber
runSqlPool action pool
runDBRead :: ReaderT SqlReadBackend Handler a -> Handler a runDBRead :: ReaderT SqlReadBackend Handler a -> Handler a
runDBRead action = do runDBRead action = do
$logDebugS "YesodPersist" "runDBRead" $logDebugS "YesodPersist" "runDBRead"
runSqlPool (withReaderT SqlReadBackend action) =<< appConnPool <$> getYesod runSqlPoolRetry (withReaderT SqlReadBackend action) =<< appConnPool <$> getYesod
-- How to run database actions. -- How to run database actions.
instance YesodPersist UniWorX where instance YesodPersist UniWorX where
@ -4580,7 +4610,9 @@ instance YesodPersist UniWorX where
let action' let action'
| dryRun = action <* transactionUndo | dryRun = action <* transactionUndo
| otherwise = action | otherwise = action
runSqlPool action' =<< appConnPool <$> getYesod
runSqlPoolRetry action' =<< appConnPool <$> getYesod
instance YesodPersistRunner UniWorX where instance YesodPersistRunner UniWorX where
getDBRunner = do getDBRunner = do
(DBRunner{..}, cleanup) <- defaultGetDBRunner appConnPool (DBRunner{..}, cleanup) <- defaultGetDBRunner appConnPool

View File

@ -66,6 +66,8 @@ import Jobs.HealthReport
import Control.Exception.Base (AsyncException) import Control.Exception.Base (AsyncException)
import Type.Reflection (typeOf)
data JobQueueException = JInvalid QueuedJobId QueuedJob data JobQueueException = JInvalid QueuedJobId QueuedJob
| JLocked QueuedJobId InstanceId UTCTime | JLocked QueuedJobId InstanceId UTCTime
@ -364,7 +366,8 @@ handleJobs' wNum = C.mapM_ $ \jctl -> withJobWorkerState wNum JobWorkerBusy $ do
lift $ foldrM (\resVar -> bool (tryPutTMVar resVar res) $ return True) False (maybe [] NonEmpty.toList resVars) lift $ foldrM (\resVar -> bool (tryPutTMVar resVar res) $ return True) False (maybe [] NonEmpty.toList resVars)
case res of case res of
Just err Just err
| not sentRes -> $logErrorS logIdent $ tshow err | not sentRes -> case err of
SomeException err' -> $logErrorS logIdent $ tshow (typeOf err') <> ": " <> pack (displayException err')
_other -> return () _other -> return ()
where where
logIdent = mkLogIdent wNum logIdent = mkLogIdent wNum