Announcing looper

Date 2019-06-14

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 =
    { 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 =
    { 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:

  :: Text	-- ^ Name
  -> LooperSettings	 
  -> m ()	-- ^ The function to loop
  -> LooperDef m	 

Running loopers

Now that you have your collection of LooperDefs, it is time to run them. You can use runLoopers to run a list of loopers.

  :: 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 =
              { looperDefName = "l1"
              , looperDefEnabled = True
              , looperDefPeriod = seconds 0.01
              , looperDefPhase = seconds 0
              , looperDefFunc =
                  atomically $ do
                    modifyTVar' v1 succ
                    modifyTVar' v2 succ
      let l2 =
              { 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)
Microsmos: Writing a simple tree-editor with brick.

Know a technical team that could use strong technical leadership?

Hire me
Cursors, Part 5: The Tree Cursor