{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-} {-| Module : Control.Concurrent.Async.Refresh Description : This module exposes the API of the async-refresh package. Copyright : (c) Moritz Schulte, 2017 License : BSD3 Maintainer : mtesseract@silverratio.net Stability : experimental Portability : POSIX The async-refresh package provides the logic for periodic refreshing of arbitrary data. This module implements the core of the package and exposes its API. -} module Control.Concurrent.Async.Refresh ( AsyncRefreshConf , AsyncRefresh , AsyncRefreshCallback , RefreshResult(..) , defaultAsyncRefreshInterval , newAsyncRefreshConf , asyncRefreshConfSetDefaultInterval , asyncRefreshConfSetLabel , asyncRefreshConfSetFactor , asyncRefreshConfSetCallback , newAsyncRefresh , asyncRefreshAsync ) where import Control.Concurrent.Async.Refresh.Prelude import Control.Concurrent.Async.Lifted.Safe (wait) import qualified Control.Concurrent.Async.Refresh.Lenses as Lens import Control.Concurrent.Async.Refresh.Types import Control.Concurrent.Async.Refresh.Util import Control.Lens -- | Given a refresh action, create a new configuration. newAsyncRefreshConf :: MonadIO m => m (RefreshResult a) -> AsyncRefreshConf m a newAsyncRefreshConf action = AsyncRefreshConf { _asyncRefreshConfDefaultInterval = defaultAsyncRefreshInterval , _asyncRefreshConfAction = action , _asyncRefreshConfCallback = defaultAsyncRefreshCallback , _asyncRefreshConfLabel = defaultAsyncRefreshLabel , _asyncRefreshConfFactor = defaultAsyncRefreshFactor } -- | Default refresh interval is one minute (in milliseconds). defaultAsyncRefreshInterval :: Int defaultAsyncRefreshInterval = 60 * 10^3 -- | Default refresh factor. See documentation for 'asyncRefreshConfSetFactor'. defaultAsyncRefreshFactor :: Double defaultAsyncRefreshFactor = 0.8 -- | The default refresh callback is a no-op. defaultAsyncRefreshCallback :: Monad m => Either SomeException (RefreshResult a) -> m () defaultAsyncRefreshCallback _ = return () -- | The default label for configuration is 'Nothing'. defaultAsyncRefreshLabel :: Maybe Text defaultAsyncRefreshLabel = Nothing -- | Set default refresh interval, specified in milliseconds, in the -- given configuration. If a refresh action fails or does not produce -- an expiry time, this interval will be used. asyncRefreshConfSetDefaultInterval :: Int -> AsyncRefreshConf m a -> AsyncRefreshConf m a asyncRefreshConfSetDefaultInterval = (Lens.defaultInterval .~) -- | Set the label in the provided configuration. This is a human -- readable text, used for logging purposes. asyncRefreshConfSetLabel :: Text -> AsyncRefreshConf m a -> AsyncRefreshConf m a asyncRefreshConfSetLabel label = Lens.label .~ Just label -- | Extract a human readable label from the provided configuration. asyncRefreshConfGetLabel :: AsyncRefreshConf m a -> Text asyncRefreshConfGetLabel conf = fromMaybe "Nothing" (conf ^. Lens.label) -- | Set the refresh factor. When a refresh gives an explicit expiry -- time after a succesful refresh run, then this expiry time will be -- multiplied by this factor, yielding the effective expiry time after -- which a new refresh run will be scheduled. asyncRefreshConfSetFactor :: Double -> AsyncRefreshConf m a -> AsyncRefreshConf m a asyncRefreshConfSetFactor factor = Lens.factor .~ restrictToInterval 0 1 factor -- | Set a refresh callback for the provided configuration. This -- callback will be called after a refresh run. asyncRefreshConfSetCallback :: AsyncRefreshCallback m a -> AsyncRefreshConf m a -> AsyncRefreshConf m a asyncRefreshConfSetCallback = (Lens.callback .~) -- | Start a new thread taking care of refreshing of data according to -- the given configuration. newAsyncRefresh :: ( MonadIO m , MonadBaseControl IO m , MonadCatch m , MonadMask m , MonadLogger m , Forall (Pure m) ) => AsyncRefreshConf m a -> m AsyncRefresh newAsyncRefresh conf = AsyncRefresh <$> async (asyncRefreshCtrlThread conf) -- | Main function of the refresh control thread. Acts as a simple -- watchdog for the thread defined by 'asyncRefreshThread' doing the -- actual work. asyncRefreshCtrlThread :: ( MonadIO m , MonadBaseControl IO m , MonadCatch m , MonadMask m , MonadLogger m , Forall (Pure m) ) => AsyncRefreshConf m a -> m () asyncRefreshCtrlThread conf = do withAsync (asyncRefreshThread conf) $ \asyncHandle -> wait asyncHandle logErrorN "Unexpected termination of async refresh thread" -- | Main function for the thread implementing the refreshing logic. asyncRefreshThread :: ( MonadIO m , MonadBaseControl IO m , MonadCatch m , MonadLogger m , Forall (Pure m) ) => AsyncRefreshConf m a -> m () asyncRefreshThread conf = forever $ tryAny (asyncRefreshDo conf) >>= \case Right res -> do let delay = fromMaybe (conf ^. Lens.defaultInterval) (refreshExpiry res) logDebugN $ sformat ("Refreshing done for refreshing request '" % stext % "'") (asyncRefreshConfGetLabel conf) threadDelay (computeRefreshTime conf delay * 10^3) Left exn -> do logErrorN $ sformat ("Refresh action failed for token request '" % stext % "': " % stext) (asyncRefreshConfGetLabel conf) (tshow exn) threadDelay (conf ^. Lens.defaultInterval * 10^3) -- | This function does the actual refreshing work. asyncRefreshDo :: ( MonadIO m , MonadBaseControl IO m , MonadCatch m , MonadLogger m , Forall (Pure m) ) => AsyncRefreshConf m a -> m (RefreshResult a) asyncRefreshDo conf = do tryA <- tryAny (conf ^. Lens.action) `logOnError` sformat ("Failed to execute refresh action for token request '" % stext % "'") (asyncRefreshConfGetLabel conf) void $ tryAny ((conf ^. Lens.callback) tryA) `logOnError` "User provided callback threw exception" either throw return tryA -- | Scale the given duration according to the factor specified in the -- configuration. computeRefreshTime :: AsyncRefreshConf m a -> Int -> Int computeRefreshTime conf duration = floor $ (conf ^. Lens.factor) * fromIntegral duration