auto-update-0.2.0: Efficiently run periodic, on-demand actions
Safe HaskellSafe-Inferred
LanguageHaskell2010

Control.Reaper

Description

This module provides the ability to create reapers: dedicated cleanup threads. These threads will automatically spawn and die based on the presence of a workload to process on. Example uses include:

  • Killing long-running jobs
  • Closing unused connections in a connection pool
  • Pruning a cache of old items (see example below)

For real-world usage, search the WAI family of packages for imports of Control.Reaper.

Synopsis

Example: Regularly cleaning a cache

In this example code, we use a Map to cache fibonacci numbers, and a Reaper to prune the cache.

NOTE: When using this module as a cache you should keep in mind that while the reaper thread is active running your "reaperAction", the cache will appear empty to concurrently running threads. Any newly created cache entries will be on the temporary worklist, and will merged back into the the main cache only once the "reaperAction" completes (together with the portion of the extant worklist that the cleaner callback decided to retain).

If you're looking for a cache that supports concurrent purging of stale items, but without exposing a transient empty cache during cleanup, this is not the cache implementation you need. This module was primarily designed for cleaning up stuck processes, or idle threads in a thread pool. The cache use-case was not a primary design focus.

The main function first creates a Reaper, with fields to initialize the cache (reaperEmpty), add items to it (reaperCons), and prune it (reaperAction). The reaper will run every two seconds (reaperDelay), but will stop running while reaperNull is true.

main then loops infinitely (forever). Each second it calculates the fibonacci number for a value between 30 and 34, first trying the cache (reaperRead and lookup), then falling back to manually calculating it (fib) and updating the cache with the result (reaperAdd)

clean simply removes items cached for more than 10 seconds. This function is where you would perform IO-related cleanup, like killing threads or closing connections, if that was the purpose of your reaper.

module Main where

import Data.Time (UTCTime, getCurrentTime, diffUTCTime)
import Control.Reaper
import Control.Concurrent (threadDelay)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Control.Monad (forever)
import System.Random (getStdRandom, randomR)

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

type Cache = Map Int (Int, UTCTime)

main :: IO ()
main = do
  reaper <- mkReaper defaultReaperSettings
    { reaperEmpty = Map.empty
    , reaperCons = \(k, v, time) workload -> Map.insert k (v, time) workload
    , reaperAction = clean
    , reaperDelay = 1000000 * 2 -- Clean every 2 seconds
    , reaperNull = Map.null
    }
  forever $ do
    fibArg <- getStdRandom (randomR (30,34))
    cache <- reaperRead reaper
    let cachedResult = Map.lookup fibArg cache
    case cachedResult of
      Just (fibResult, _createdAt) -> putStrLn $ "Found in cache: `fib " ++ show fibArg ++ "` " ++ show fibResult
      Nothing -> do
        let fibResult = fib fibArg
        putStrLn $ "Calculating `fib " ++ show fibArg ++ "` " ++ show fibResult
        time <- getCurrentTime
        (reaperAdd reaper) (fibArg, fibResult, time)
    threadDelay 1000000 -- 1 second

-- Remove items > 10 seconds old
clean :: Cache -> IO (Cache -> Cache)
clean oldMap = do
  currentTime <- getCurrentTime
  let pruned = Map.filter (\(_, createdAt) -> currentTime `diffUTCTime` createdAt < 10.0) oldMap
  return (\newData -> Map.union pruned newData)

Settings

data ReaperSettings workload item Source #

Settings for creating a reaper. This type has two parameters: workload gives the entire workload, whereas item gives an individual piece of the queue. A common approach is to have workload be a list of items. This is encouraged by defaultReaperSettings and mkListAction.

Since: 0.1.1

defaultReaperSettings :: ReaperSettings [item] item Source #

Default ReaperSettings value, biased towards having a list of work items.

Since: 0.1.1

Accessors

reaperAction :: ReaperSettings workload item -> workload -> IO (workload -> workload) Source #

The action to perform on a workload. The result of this is a "workload modifying" function. In the common case of using lists, the result should be a difference list that prepends the remaining workload to the temporary workload. The temporary workload here refers to items added to the workload while the reaper action is running. For help with setting up such an action, see mkListAction.

Default: do nothing with the workload, and then prepend it to the temporary workload. This is incredibly useless; you should definitely override this default.

Since: 0.1.1

reaperDelay :: ReaperSettings workload item -> Int Source #

Number of microseconds to delay between calls of reaperAction.

Default: 30 seconds.

Since: 0.1.1

reaperCons :: ReaperSettings workload item -> item -> workload -> workload Source #

Add an item onto a workload.

Default: list consing.

Since: 0.1.1

reaperNull :: ReaperSettings workload item -> workload -> Bool Source #

Check if a workload is empty, in which case the worker thread will shut down.

Default: null.

Since: 0.1.1

reaperEmpty :: ReaperSettings workload item -> workload Source #

An empty workload.

Default: empty list.

Since: 0.1.1

Type

data Reaper workload item Source #

A data structure to hold reaper APIs.

reaperAdd :: Reaper workload item -> item -> IO () Source #

Adding an item to the workload

reaperRead :: Reaper workload item -> IO workload Source #

Reading workload.

reaperModify :: Reaper workload item -> (workload -> workload) -> IO workload Source #

Modify the workload. The resulting workload is returned.

If there is no reaper thread, the modifier will not be applied and reaperEmpty will be returned.

If the reaper is currently executing jobs, those jobs will not be in the given workload and the workload might appear empty.

If all jobs are removed by the modifier, the reaper thread will not be killed. The reaper thread will only terminate if reaperKill is called or the result of reaperAction satisfies reaperNull.

Since: 0.2.0

reaperStop :: Reaper workload item -> IO workload Source #

Stopping the reaper thread if exists. The current workload is returned.

reaperKill :: Reaper workload item -> IO () Source #

Killing the reaper thread immediately if exists.

Creation

mkReaper :: ReaperSettings workload item -> IO (Reaper workload item) Source #

Create a reaper addition function. This function can be used to add new items to the workload. Spawning of reaper threads will be handled for you automatically.

Since: 0.1.1

Helper

mkListAction :: (item -> IO (Maybe item')) -> [item] -> IO ([item'] -> [item']) Source #

A helper function for creating reaperAction functions. You would provide this function with a function to process a single work item and return either a new work item, or Nothing if the work item is expired.

Since: 0.1.1