blob: b714d71e1024db0939bc693d8b29bd87918a11d7 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
{-# LANGUAGE CPP #-}
module PingMachine where
import Control.Monad
import Data.Function
#ifdef THREAD_DEBUG
import Control.Concurrent.Lifted.Instrument
#else
import Control.Concurrent.Lifted
import GHC.Conc (labelThread)
#endif
import Control.Concurrent.STM
import InterruptibleDelay
type Miliseconds = Int
type TimeOut = Miliseconds
type PingInterval = Miliseconds
-- | Events that occur as a result of the 'PingMachine' watchdog.
--
-- Use 'pingWait' to wait for one of these to occur.
data PingEvent
= PingIdle -- ^ You should send a ping if you observe this event.
| PingTimeOut -- ^ You should give up on the connection in case of this event.
data PingMachine = PingMachine
{ pingFlag :: TVar Bool
, pingInterruptible :: InterruptibleDelay
, pingEvent :: TMVar PingEvent
, pingStarted :: TMVar Bool
}
-- | Fork a thread to monitor a connection for a ping timeout.
--
-- If 'pingBump' is not invoked after a idle is signaled, a timeout event will
-- occur. When that happens, even if the caller chooses to ignore this event,
-- the watchdog thread will be terminated and no more ping events will be
-- signaled.
--
-- An idle connection will be signaled by:
--
-- (1) 'pingFlag' is set 'True'
--
-- (2) 'pingWait' returns 'PingIdle'
--
-- Either may be tested to determine whether a ping should be sent but
-- 'pingFlag' is difficult to use properly because it is up to the caller to
-- remember that the ping is already in progress.
forkPingMachine
:: PingInterval -- ^ Milliseconds of idle before a ping is considered necessary.
-> TimeOut -- ^ Milliseconds after 'PingIdle' before we signal 'PingTimeOut'.
-> IO PingMachine
forkPingMachine idle timeout = do
d <- interruptibleDelay
flag <- atomically $ newTVar False
canceled <- atomically $ newTVar False
event <- atomically newEmptyTMVar
started <- atomically $ newEmptyTMVar
when (idle/=0) $ void . forkIO $ do
myThreadId >>= flip labelThread ("ping.watchdog")
(>>=) (atomically (readTMVar started)) $ flip when $ do
fix $ \loop -> do
atomically $ writeTVar flag False
fin <- startDelay d (1000*idle)
(>>=) (atomically (readTMVar started)) $ flip when $ do
if (not fin) then loop
else do
-- Idle event
atomically $ do
tryTakeTMVar event
putTMVar event PingIdle
writeTVar flag True
fin <- startDelay d (1000*timeout)
(>>=) (atomically (readTMVar started)) $ flip when $ do
me <- myThreadId
if (not fin) then loop
else do
-- Timeout event
atomically $ do
tryTakeTMVar event
writeTVar flag False
putTMVar event PingTimeOut
return PingMachine
{ pingFlag = flag
, pingInterruptible = d
, pingEvent = event
, pingStarted = started
}
-- | Terminate the watchdog thread. Call this upon connection close.
--
-- You should ensure no threads are waiting on 'pingWait' because there is no
-- 'PingEvent' signaling termination.
pingCancel :: PingMachine -> IO ()
pingCancel me = do
atomically $ do tryTakeTMVar (pingStarted me)
putTMVar (pingStarted me) False
interruptDelay (pingInterruptible me)
-- | Reset the ping timer. Call this regularly to prevent 'PingTimeOut'.
pingBump :: PingMachine -> IO ()
pingBump me = do
atomically $ do
b <- tryReadTMVar (pingStarted me)
when (b/=Just False) $ do
tryTakeTMVar (pingStarted me)
putTMVar (pingStarted me) True
interruptDelay (pingInterruptible me)
-- | Retries until a 'PingEvent' occurs.
pingWait :: PingMachine -> STM PingEvent
pingWait me = takeTMVar (pingEvent me)
|