{-# LANGUAGE OverloadedStrings, NamedFieldPuns #-}

-- | This module implements exponential backoff so as to prevent
--   spamming of steal actions.  This is always a good idea, and
--   especially so in the distributed case where steal attempts send
--   actual messages.
--
--   Normally backoff functionality is baked into the scheduler loop.
--   One nice aspect of the Meta scheduler design is that backoff can
--   become "just another resource".  Most schedulers (compositions)
--   should include this at tho bottom of their stack.

module Control.Monad.Par.Meta.Resources.Backoff ( defaultStartup
                                                , mkWorkSearch
                                                , mkResource
                                                ) where 

import Data.IORef (readIORef)
import Data.Word (Word64)
import Control.Monad.Par.Meta
import Control.Monad.Par.Meta.Resources.Debugging (dbgTaggedMsg)
import Control.Concurrent     (threadDelay)
import qualified Data.ByteString.Char8 as BS

mkResource :: Word64 -> Word64 -> Resource
mkResource shortest longest =
  Resource defaultStartup (mkWorkSearch shortest longest)

defaultStartup :: Startup
defaultStartup = St (\ _ _ -> return () )

-- | To construct a WorkSearch we need to know the minimum and
-- maximum amount of time (nanoseconds) to sleep.  The exponential
-- backoff policy is always the same: it starts at 1ns and doubles.
-- 
-- The thing that changes over time is whether sleeping actually
-- *occurs*.  For example, `mkWorkSearch 1000 100000` will not sleep
-- for the first ten invocations (until 1024), and then will sleep an
-- amount that doubles each time until it surpasses the maximum, at
-- which point each sleep will be for the maximum: 100ms.
mkWorkSearch :: Word64 -> Word64 -> WorkSearch
-- Sleeping ZERO time means not sleeping at all:
mkWorkSearch _        0       = WS$ \ _ _ -> return Nothing
mkWorkSearch shortest longest = WS ws 
  where 
    ws Sched{consecutiveFailures} _ = do
      failCount <- readIORef consecutiveFailures
      let nanos  = if failCount >= 64
                   then longest 
                   else 2 ^ failCount
      if nanos >= shortest then do 
         let capped = min longest nanos
         dbgTaggedMsg 3 $ "Backoff: Sleeping, nanoseconds = " `BS.append` BS.pack (show capped)
         threadDelay (fromIntegral capped)
       else do 
         dbgTaggedMsg 4 $ "Backoff: NOT yet sleeping, nanoseconds = " `BS.append` BS.pack (show nanos)
         -- QUESTION - do we want to yield here?  Probably not.
         -- Maybe for some ranges of nanosecond intervals we could yield instead...
         -- yield
      return Nothing