API速率限制器间歇性悬挂

时间:2018-04-02 22:07:36

标签: f# mailboxprocessor

我编写了一个简单的(我认为...)速率限制器,以将事件驱动系统保持在我们的许可API命中限制之下。出于某种原因,在发送400-500个请求后,它有时会

我最好的想法是我搞砸了等待功能所以在某些情况下它永远不会返回,但我无法找到有缺陷的逻辑。另一个想法是我拙劣的异步/任务互操作导致问题。它总是先工作然后再工作。单个实例ApiRateLimiter正在多个组件之间共享,以便在系统范围内实现命中限制。

type RequestWithReplyChannel = RequestWithKey * AsyncReplyChannel<ResponseWithKey>

type public ApiRateLimiter(httpClient: HttpClient, limitTimePeriod: TimeSpan, limitCount: int) =

let requestLimit = Math.Max(limitCount,1)

let agent = MailboxProcessor<RequestWithReplyChannel>.Start(fun inbox -> 

    let rec waitUntilUnderLimit (recentRequestsTimeSent: seq<DateTimeOffset>) = async{
        let cutoffTime = DateTimeOffset.UtcNow.Subtract limitTimePeriod
        let requestsWithinLimit = 
            recentRequestsTimeSent 
            |> Seq.filter(fun x -> x >= cutoffTime)
            |> Seq.toList

        if requestsWithinLimit.Length >= requestLimit then
            let! _ = Async.Sleep 100 //sleep for 100 milliseconds and check request limit again
            return! waitUntilUnderLimit requestsWithinLimit
        else
            return requestsWithinLimit
    }

    let rec messageLoop (mostRecentRequestsTimeSent: seq<DateTimeOffset>) = async{
        // read a message
        let! keyedRequest,replyChannel = inbox.Receive()
        // wait until we are under our rate limit
        let! remainingRecentRequests = waitUntilUnderLimit mostRecentRequestsTimeSent

        let rightNow = DateTimeOffset.UtcNow

        let! response =
            keyedRequest.Request
            |> httpClient.SendAsync
            |> Async.AwaitTask

        replyChannel.Reply { Key = keyedRequest.Key; Response = response }

        return! messageLoop (seq {
            yield rightNow
            yield! remainingRecentRequests
        })
    }

    // start the loop
    messageLoop (Seq.empty<DateTimeOffset>)
)            

member this.QueueApiRequest keyedRequest =
    async {
        return! agent.PostAndAsyncReply(fun replyChannel -> (keyedRequest,replyChannel))
    } |> Async.StartAsTask

有些请求很大并需要一点时间,但是没有什么可能导致请求发送完全死亡,我看到这件事。

感谢您花点时间看看!

1 个答案:

答案 0 :(得分:3)

我注意到您正在构建一个使用seq发送请求的最近时间列表:

seq {
    yield rightNow
    yield! remainingRecentRequests
}

因为F#序列是惰性的,所以这会产生一个枚举器,当被要求它的下一个值时,它将首先产生一个值,然后将开始迭代其子seq并产生一个值。每次产生新请求时,都会添加一个新的枚举器 - 但旧的枚举器何时处理?您认为一旦它们过期就会被处理掉,也就是说,Seq.filter中的waitUntilUnderLimit返回false。但请想一想:F#编译器如何知道过滤条件一旦出现错误就会始终为false?如果没有深入的代码分析(编译器不做),它就不能。因此,“旧的”seqs永远不会被垃圾收集,因为它们仍然被保留,以防万一它们被需要。我不是百分之百肯定的,因为我没有测量你的代码的内存使用情况,但是如果你要衡量你的ApiRateLimiter实例的内存使用情况,我打赌你会看到它稳定增长而不会去下来。

我还注意到您要在seq的前面上添加新项目。这与F#列表将使用的语义完全相同,但是对于列表,没有要分配的IEnumerable对象,并且一旦列表项失败List.filter条件,它将被丢弃。因此,我重写了您的代码以使用最近时间而不是seq的列表,并且我为效率做了另外一个更改:因为您创建列表的方式保证它将被排序,最近的事件是最先发生的,最后的最后一个,我将List.filter替换为List.takeWhile。这样,当第一个日期早于截止日期时,它将停止检查较旧的日期。

通过此更改,您现在应该有旧日期实际到期,并且ApiRateLimiter类的内存使用量应该会波动,但仍保持不变。 (每次调用waitUntilUnderLimit时都会创建新列表,因此会产生一些GC压力,但这些压力都应该在第0代中。)我不知道这是否能解决您的悬挂问题,但这是我在您的代码中看到的唯一问题。

顺便说一句,我还用[{1}}替换了您的行let! _ = Async.Sleep 100,这更简单。这里没有效率提升,但是没有必要使用do! Async.Sleep 100等待let! _ =返回;这正是Async<unit>关键字的用途。

do!