Control.Reaper

auto-update-0.2.6: Efficiently run periodic, on-demand actions

Safe Haskell | None
Language | Haskell2010

Control.Reaper

Contents

- Example: Regularly cleaning a cache
- Settings
- Accessors
- Type
- Creation
- Helper

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

- data ReaperSettings workload item
- defaultReaperSettings :: ReaperSettings [item] item
- reaperAction :: ReaperSettings workload item -> workload -> IO (workload -> workload)
- reaperDelay :: ReaperSettings workload item -> Int
- reaperCons :: ReaperSettings workload item -> item -> workload -> workload
- reaperNull :: ReaperSettings workload item -> workload -> Bool
- reaperEmpty :: ReaperSettings workload item -> workload
- reaperThreadName :: ReaperSettings workload item -> String
- data Reaper workload item
- reaperAdd :: Reaper workload item -> item -> IO ()
- reaperRead :: Reaper workload item -> IO workload
- reaperModify :: Reaper workload item -> (workload -> workload) -> IO workload
- reaperStop :: Reaper workload item -> IO workload
- reaperKill :: Reaper workload item -> IO ()
- mkReaper :: ReaperSettings workload item -> IO (Reaper workload item)
- mkListAction :: (item -> IO (Maybe item')) -> [item] -> IO ([item'] -> [item'])

# 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 #

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 `item`s. This is encouraged by `defaultReaperSettings` and `mkListAction`.

Since: 0.1.1

defaultReaperSettings :: ReaperSettings [item] item #

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

Since: 0.1.1

# Accessors

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

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 #

Number of microseconds to delay between calls of `reaperAction`.

Default: 30 seconds.

Since: 0.1.1

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

Add an item onto a workload.

Default: list consing.

Since: 0.1.1

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

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 #

An empty workload.

Default: empty list.

Since: 0.1.1

reaperThreadName :: ReaperSettings workload item -> String #

Label of the thread spawned by the reaper.

Default: `Reaper`.

Since: 0.2.2

# Type

data Reaper workload item #

A data structure to hold reaper APIs.

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

Adding an item to the workload

reaperRead :: Reaper workload item -> IO workload #

Reading workload.

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

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 #

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

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

Killing the reaper thread immediately if exists.

# Creation

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

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']) #

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