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 =
  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 LooperDefs, 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)
Previous
Microsmos: Writing a simple tree-editor with brick.

Looking for a lead engineer?

Hire me
Next
Cursors, Part 5: The Tree Cursor