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:
- An
optparse-applicative
parser forLooperFlags
- A parser for for
LooperEnvironment
- A
FromJSON
instance forLooperConfiguration
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 =
describe "runLoopers" $
it "runs two loopers as intended" $ do
v1 <- newTVarIO (0 :: Int)
v2 <- newTVarIO (0 :: Int)
v3 <- newTVarIO (0 :: Int)
let l1 =
LooperDef
{ looperDefName = "l1"
, looperDefEnabled = True
, looperDefPeriod = seconds 0.01
, looperDefPhase = seconds 0
, looperDefFunc =
atomically $ do
modifyTVar' v1 succ
modifyTVar' v2 succ
}
let l2 =
LooperDef
{ looperDefName = "l2"
, looperDefEnabled = True
, looperDefPeriod = seconds 0.005
, looperDefPhase = seconds 0.005
, looperDefFunc =
atomically $ do
modifyTVar' v2 succ
modifyTVar' v3 succ
}
a <- async $ runLoopers [l1, l2]
waitNominalDiffTime $ seconds 0.0225
cancel a
r1 <- readTVarIO v1
r2 <- readTVarIO v2
r3 <- readTVarIO v3
(r1, r2, r3) `shouldBe` (3, 7, 4)