packages feed

freckle-app-1.0.2.3: library/Freckle/App/GlobalCache.hs

-- | Facility for purely creating an 'IORef' in which to stash a value
--
-- Storing a truly global value can be useful for performance (e.g. to speed up
-- tests by caching a constructed App) or necessary to prevent contention (e.g.
-- by caching a single 'LoggerSet' which holds access to a log file).
--
-- In some cases, it's not possible to create an 'IORef' safely in an 'IO'
-- context to use for this purpose. Either because a library prevents it (e.g.
-- the test runner provides no such hook before triggering its parallel
-- execution) or because the current application architecture cannot allow it
-- without a high effort re-organization.
--
-- For these cases, we use this module.
--
-- == Usage
--
-- Given some function,
--
-- @
-- makeLogger :: HasLogging a => a -> IO Logger
-- makeLogger app = makeYesodLogger =<< newLoggerSet defaultBufSize
--  where
--   newLoggerSet = case getLogLocation app of
--     LogStdout -> newStdoutLoggerSet
--     LogStderr -> newStderrLoggerSet
--     LogFile f -> flip newFileLoggerSet f
-- @
--
-- Update it to cache the construction in a new top-level value,
--
-- @
-- loggerSetVar :: GlobalCache LoggerSet
-- loggerSetVar = unsafePerformIO newGlobalCache
-- {-# NOINLINE loggerSetVar #-}
--
-- makeLogger :: HasLogging a => a -> IO Logger
-- makeLogger app = makeYesodLogger
--   =<< globallyCache loggerSetVar (newLoggerSet defaultBufSize)
--  where
--   newLoggerSet = case getLogLocation app of
--     LogStdout -> newStdoutLoggerSet
--     LogStderr -> newStderrLoggerSet
--     LogFile f -> flip newFileLoggerSet f
-- @
--
module Freckle.App.GlobalCache
  ( GlobalCache
  , newGlobalCache
  , globallyCache
  , withGlobalCacheCleanup
  ) where

import Freckle.App.Prelude

import Control.Concurrent.MVar (mkWeakMVar, modifyMVar_, newMVar)
import Data.IORef

newtype GlobalCache a = GlobalCache
  { _unGlobalCache :: IORef (Maybe a)
  }

newGlobalCache :: IO (GlobalCache a)
newGlobalCache = GlobalCache <$> newIORef Nothing

globallyCache :: GlobalCache a -> IO a -> IO a
globallyCache (GlobalCache var) construct = do
  mv <- readIORef var
  maybe (cache =<< construct) pure mv
  where cache v = atomicModifyIORef' var $ const (Just v, v)

-- | Garbage collect one of our 'IORef's after an action has run
--
-- Global 'IORef's are problematic for @ghci@. @ghci@ cannot garbage collect
-- them, so any state they hold will persist for an entire @ghci@ session. This
-- causes a varietyof issues.
--
-- To avoid garbage collection issues, we can leverage a "System.Mem.Weak" to
-- add a finalizer. When that 'MVar' gets garbage collected we can clear the
-- global 'IORef'. This maintains the status quo, with minimal plumbing.
--
withGlobalCacheCleanup :: GlobalCache a -> IO b -> IO ()
withGlobalCacheCleanup (GlobalCache var) action = do
  cleanup <- newMVar ()
  void $ mkWeakMVar cleanup $ writeIORef var Nothing
  void action
  modifyMVar_ cleanup (const $ pure ())