我试图编写一个方便的抽象来支持api调用。 API调用将是某种HTTP请求。我希望能够编写所有应用程序逻辑,就好像我只是在IO
Monad中一样,然后让抽象方式限制我的调用,因此我不会超过某些预定义的限制。让这些调用以异步方式运行会很好。现在我有了这个。
data APICallStats = APICallStats
{
startTime :: !UTCTime
, requestCount :: !Int
, tps :: !Int
} deriving Show
newtype APICall a = APICall (IO a)
initStats :: Int -> IO APICallStats
initStats limit = APICallStats <$> getCurrentTime <*> pure 0 <*> pure limit
makeCall :: MVar APICallStats -> APICall a -> IO a
makeCall mv (APICall f) = do
(APICallStats st c t) <- readMVar mv
now <- getCurrentTime
let inSeconds = realToFrac (diffUTCTime now st) :: Double
cp1 = fromIntegral (c+1)
td = fromIntegral t
when (cp1 / inSeconds > td)
(threadDelay (round $ (cp1 / td - inSeconds)*1000000))
modifyMVar_ mv
(\(APICallStats start req ts) -> return $ APICallStats start (req+1) ts)
f
我可以像这样测试它。由于MVar
,它在单个或多个线程中工作正常。
testCall :: String -> APICall ()
testCall id = APICall (getCurrentTime >>= (putStrLn . ((++) (id ++ " ")) . show))
test :: IO ()
test = do
mv <- initStats 1 >>= newMVar
forever $ makeCall mv (testCall "")
threadedTest :: IO ()
threadedTest = do
mv <- initStats 1 >>= newMVar
threadId <- forkIO $ forever $ makeCall mv (testCall "thread0")
forever $ makeCall mv (testCall "main thread")
killThread threadId
这很好,除了它不像我想要的那样抽象。由于只有几个特定的API调用,我可以让它们返回类型APICall a
。然后将其作为MonadIO
实例的monad,这样我就可以编写逻辑并在需要时使用liftIO
。但是我不确定我是否能够将这个单子作为一个没有违反法律以及如何去做的单子。
修改
我认为我已经非常接近我想要它做的事了。
withThrottle :: Int -> StateT (MVar APICallStats) IO a -> IO a
withThrottle limit f = do
mv <- initStats limit >>= newMVar
evalStateT f mv
process :: APICall a -> StateT (MVar APICallStats) IO a
process a = do
mv <- get
liftIO $ makeCall mv a
有了这个,我可以写这样的东西。
stateTest = do
withThrottle 2 $ do
process (testCall "")
process (testCall "")
process (testCall "")
liftIO $ threadDelay 10000000 -- Some long computation
process (testCall "")
process (testCall "")
process (testCall "")
process (testCall "")
使用长时间运行的应用程序,整个事情可以运行的是这个monad。这将保证它在服务的生命周期内不会进行太多的外部API调用。我只是不想要国家monad。只是MonadIO
的东西。所以我想我只需要隐藏它,我就会完成。但解决方案并不是非常漂亮,所以欢迎其他建议。