never executed always true always false
1 module PureClaw.Scheduler.Heartbeat
2 ( -- * Heartbeat configuration
3 HeartbeatConfig (..)
4 , defaultHeartbeatConfig
5 -- * Heartbeat runner
6 , HeartbeatState (..)
7 , mkHeartbeatState
8 , runHeartbeat
9 , stopHeartbeat
10 , isHeartbeatRunning
11 -- * Single tick (for testing)
12 , heartbeatTick
13 ) where
14
15 import Control.Concurrent
16 import Control.Concurrent.STM
17 import Control.Exception
18 import Data.Text (Text)
19 import Data.Text qualified as T
20 import Data.Time
21
22 import PureClaw.Handles.Log
23
24 -- | Configuration for the heartbeat scheduler.
25 data HeartbeatConfig = HeartbeatConfig
26 { _hb_intervalSeconds :: Int
27 , _hb_name :: Text
28 }
29 deriving stock (Show, Eq)
30
31 -- | Default heartbeat: every 60 seconds.
32 defaultHeartbeatConfig :: HeartbeatConfig
33 defaultHeartbeatConfig = HeartbeatConfig
34 { _hb_intervalSeconds = 60
35 , _hb_name = "heartbeat"
36 }
37
38 -- | Mutable heartbeat state.
39 data HeartbeatState = HeartbeatState
40 { _hbs_config :: HeartbeatConfig
41 , _hbs_running :: TVar Bool
42 , _hbs_lastTick :: TVar (Maybe UTCTime)
43 , _hbs_tickCount :: TVar Int
44 }
45
46 -- | Create a fresh heartbeat state.
47 mkHeartbeatState :: HeartbeatConfig -> IO HeartbeatState
48 mkHeartbeatState config = HeartbeatState config
49 <$> newTVarIO False
50 <*> newTVarIO Nothing
51 <*> newTVarIO 0
52
53 -- | Run the heartbeat loop in a new thread. The provided action is
54 -- called on each tick. Returns the thread ID. Use 'stopHeartbeat' to
55 -- stop the loop.
56 runHeartbeat :: HeartbeatState -> LogHandle -> IO () -> IO ThreadId
57 runHeartbeat hbs lh action = do
58 atomically $ writeTVar (_hbs_running hbs) True
59 _lh_logInfo lh $ "Heartbeat started: " <> _hb_name (_hbs_config hbs)
60 forkIO $ heartbeatLoop hbs lh action
61
62 -- | Stop the heartbeat loop.
63 stopHeartbeat :: HeartbeatState -> IO ()
64 stopHeartbeat hbs = atomically $ writeTVar (_hbs_running hbs) False
65
66 -- | Check if the heartbeat is running.
67 isHeartbeatRunning :: HeartbeatState -> IO Bool
68 isHeartbeatRunning hbs = readTVarIO (_hbs_running hbs)
69
70 -- | Execute a single heartbeat tick. Updates last tick time and count.
71 -- Returns 'True' if the action completed successfully.
72 heartbeatTick :: HeartbeatState -> LogHandle -> IO () -> IO Bool
73 heartbeatTick hbs lh action = do
74 now <- getCurrentTime
75 result <- try action
76 case result of
77 Left (e :: SomeException) -> do
78 _lh_logError lh $ "Heartbeat tick failed: " <> T.pack (show e)
79 pure False
80 Right () -> do
81 atomically $ do
82 writeTVar (_hbs_lastTick hbs) (Just now)
83 modifyTVar' (_hbs_tickCount hbs) (+ 1)
84 pure True
85
86 -- Internal: the heartbeat loop.
87 heartbeatLoop :: HeartbeatState -> LogHandle -> IO () -> IO ()
88 heartbeatLoop hbs lh action = do
89 running <- readTVarIO (_hbs_running hbs)
90 if running
91 then do
92 _ <- heartbeatTick hbs lh action
93 threadDelay (intervalMicros hbs)
94 heartbeatLoop hbs lh action
95 else
96 _lh_logInfo lh $ "Heartbeat stopped: " <> _hb_name (_hbs_config hbs)
97
98 -- Internal: convert interval seconds to microseconds.
99 intervalMicros :: HeartbeatState -> Int
100 intervalMicros hbs = _hb_intervalSeconds (_hbs_config hbs) * 1000000