This post announces the new looper
library. It is a small library to define actions that need to run periodically.
Looper was born out of a need that I had had multiple times while developing a product, be it Intray, Tickler, Snotify or a client project.
While writing a web service, it often came up that periodic actions needed to happen. Tickler, for example, needs to check whether to trigger any items periodically. Snotify checks for updates to users' repositories periodically. I call these periodic actions "Loopers", so Tickler has a "Triggerer" looper and Snotify has an "Analysis" looper.
In looper
, a looper is defined as follows:
data LooperDef m =
LooperDef
looperDefName :: Text -- ^ The name of the looper, can be useful for logging
{ looperDefEnabled :: Bool -- ^ Whether this looper is enabled
, looperDefPeriod :: NominalDiffTime -- ^ The time between the start of each run
, looperDefPhase :: NominalDiffTime -- ^ The time before the first run
, looperDefFunc :: m () -- ^ The function to run
,
}deriving (Generic)
Flags, Environment variables and Configuration
Whether these loopers run needs to be configurable via command-line flags, environment variables and a configuration file. The looper
library provides some convenience functions for each of these:
You can then put these together with a little convenience function that combines the three to LooperSettings
:
data LooperSettings =
LooperSettings
looperSetEnabled :: Bool
{ looperSetPhase :: NominalDiffTime
, looperSetPeriod :: NominalDiffTime
,
}deriving (Show, Eq, Generic)
deriveLooperSettings ::
NominalDiffTime -- ^ Default phase
-> NominalDiffTime -- ^ Default period
-> LooperFlags
-> LooperEnvironment
-> Maybe LooperConfiguration
-> LooperSettings
Once you have LooperSettings
, you can make a LooperDef
by combining the settings, a name and the function to run periodically using mkLooperDef
:
mkLooperDef :: Text -- ^ Name
-> LooperSettings
-> m () -- ^ The function to loop
-> LooperDef m
Running loopers
Now that you have your collection of LooperDef
s, it is time to run them. You can use runLoopers
to run a list of loopers.
runLoopers :: MonadUnliftIO m => [LooperDef m] -> m ()
Note that this function will loop infinitely (as intended), so you will need to somehow run it asynchronously if that's what you intended.
There are also some more advanced combinators that allow you to run loopers in such a way that you can add your own logging or metrics around them. See runLoopersRaw
for more details.
More examples
For more examples of how to use this library and how it works, be sure to have a look at the tests.
Here is an example test that runs two loopers:
spec :: Spec
=
spec "runLoopers" $
describe "runs two loopers as intended" $ do
it <- newTVarIO (0 :: Int)
v1 <- newTVarIO (0 :: Int)
v2 <- newTVarIO (0 :: Int)
v3 let l1 =
LooperDef
= "l1"
{ looperDefName = True
, looperDefEnabled = seconds 0.01
, looperDefPeriod = seconds 0
, looperDefPhase =
, looperDefFunc $ do
atomically succ
modifyTVar' v1 succ
modifyTVar' v2
}let l2 =
LooperDef
= "l2"
{ looperDefName = True
, looperDefEnabled = seconds 0.005
, looperDefPeriod = seconds 0.005
, looperDefPhase =
, looperDefFunc $ do
atomically succ
modifyTVar' v2 succ
modifyTVar' v3
}<- async $ runLoopers [l1, l2]
a $ seconds 0.0225
waitNominalDiffTime
cancel a<- readTVarIO v1
r1 <- readTVarIO v2
r2 <- readTVarIO v3
r3 `shouldBe` (3, 7, 4) (r1, r2, r3)