-- |This module implements rate-limiting functionality for Haskell programs.-- Rate-limiting is useful when trying to control / limit access to a-- particular resource over time. For example, you might want to limit the-- rate at which you make requests to a server, as an administrator may block-- your access if you make too many requests too quickly. Similarly, one may-- wish to rate-limit certain communication actions, in order to avoid-- accidentally performing a denial-of-service attack on a critical resource.---- The fundamental idea of this library is that given some basic information-- about the requests you wante rate limited, it will return you a function-- that hides all the rate-limiting detail. In short, you make a call to one-- of the function generators in this file, and you will be returned a function-- to use. For example:---- @-- do f <- generateRateLimitedFunction ...-- ...-- res1 <- f a-- ...-- res2 <- f b-- ...-- @---- The calls to the generated function (f) will be rate limited based on the-- parameters given to 'generateRateLimitedFunction'.---- 'generateRateLimitedFunction' is the most general version of the rate-- limiting functionality, but specialized versions of it are also exported-- for convenience.--moduleControl.RateLimit(generateRateLimitedFunction,RateLimit,ResultsCombiner,dontCombine,rateLimitInvocation,rateLimitExecution)whereimportControl.ConcurrentimportControl.Concurrent.ChanimportControl.Monad(when)importData.Time.Units-- |The rate at which to limit an action.dataTimeUnita=>RateLimita=PerInvocationa-- ^Rate limit the action to invocation once per time-- unit. With this option, the time it takes for the-- action to take place is not taken into consideration-- when computing the rate, only the time between -- invocations of the action. This may cause the action-- to execute concurrently, as an invocation may occur-- while an action is still running.|PerExecutiona-- ^Rate limit the action to execution once per time-- unit. With this option, the time it takes for the-- action to take plase is taken into account, and all-- actions will necessarily occur sequentially. However,-- if your action takes longer than the time unit given,-- then the rate of execution will be slower than the-- given unit of time.-- |In some cases, if two requests are waiting to be run, it may be possible-- to combine them into a single request and thus increase the overall-- bandwidth. The rate limit system supports this, but requires a little-- additional information to make everything work out right. You may also-- need to do something a bit wonky with your types to make this work ... -- sorry.---- The basic idea is this: Given two requests, you can either return Nothing-- (signalling that the two requests can be combined), or a Just with a new-- request representing the combination of the two requests. In addition, you-- will need to provide a function that can turn the response to this single-- request into two responses, one for each of the original requests.---- I hope this description helps you work through the type, which I'll admit-- is a bit opaque.typeResultsCombinerreqresp=req->req->Maybe(req,resp->(resp,resp))dontCombine::ResultsCombinerabdontCombine__=Nothing-- |Rate limit the invocation of a given action. This is equivalent to calling-- 'generateRateLimitedFunction' with a 'PerInvocation' rate limit and the-- 'dontCombine' combining function.rateLimitInvocation::TimeUnitt=>t->(req->IOresp)->IO(req->IOresp)rateLimitInvocationpertimeaction=generateRateLimitedFunction(PerInvocationpertime)actiondontCombine-- |Rate limit the execution of a given action. This is equivalent to calling-- 'generateRateLimitedFunction' with a 'PerExecution' rate limit and the-- 'dontCombine' combining function.rateLimitExecution::TimeUnitt=>t->(req->IOresp)->IO(req->IOresp)rateLimitExecutionpertimeaction=generateRateLimitedFunction(PerExecutionpertime)actiondontCombine-- |The most generic way to rate limit an invocation.generateRateLimitedFunction::TimeUnitt=>RateLimitt-- ^What is the rate limit for this action->(req->IOresp)-- ^What is the action you want to rate limit, given as an-- a MonadIO function from requests to responses?->ResultsCombinerreqresp-- ^A function that can combine requests if rate-- limiting happens. If you cannot combine two-- requests into one request, we suggest using-- 'dontCombine'.->IO(req->IOresp)generateRateLimitedFunctionratelimitactioncombiner=dochan<-newChanforkIO$runner(-42)chanreturn$resultFunctionchanwhererunnerlastTimechan=do-- should we wait for some amount of time?now<-toMicroseconds`fmap`(getCPUTimeWithUnit::IOMicrosecond)when(now-lastTime<toMicroseconds(getRateratelimit))$doletdelay=toMicroseconds(getRateratelimit)-(now-lastTime)threadDelay(fromIntegraldelay)-- OK, we're ready for the next item(req,respMV)<-readChanchanletbaseHandlerresp=putMVarrespMVresp-- can we combine this with any other requests on the pipe?(req',finalHandler)<-updateRequestWithFollowerschanreqbaseHandlerifshouldForkratelimitthenforkIO(actionreq'>>=finalHandler)>>return()elseactionreq'>>=finalHandlernextTime<-toMicroseconds`fmap`(getCPUTimeWithUnit::IOMicrosecond)runnernextTimechan-- updateRequestWithFollowers: We have one request. Can we combine it with-- some other requests into a cohesive whole?updateRequestWithFollowerschanreqhandler=doisEmpty<-isEmptyChanchanifisEmptythenreturn(req,handler)elsedo(next,nextRespMV)<-readChanchancasecombinerreqnextofNothing->dounGetChanchan(next,nextRespMV)return(req,handler)Just(req',splitResponse)->updateRequestWithFollowerschanreq'$\resp->dolet(theirs,mine)=splitResponserespputMVarnextRespMVminehandlertheirs-- shouldFork: should we fork or execute the action in place?shouldFork(PerInvocation_)=TrueshouldFork(PerExecution_)=False-- getRate: what is the rate of this action?getRate(PerInvocationx)=xgetRate(PerExecutionx)=x-- resultFunction: the function (partially applied on the channel) that will-- be returned from this monstrosity.resultFunctionchanreq=dorespMV<-newEmptyMVarwriteChanchan(req,respMV)takeMVarrespMV