如何正确地限制从WebJobs访问DocumentDb

时间:2015-08-31 21:29:40

标签: c# azure system.reactive azure-webjobs azure-cosmosdb

我有一个带有blob和队列触发器的Azure WebKob,用于将数据保存到Azure DocumentDb。

我不时会收到错误:

  

Microsoft.Azure.Documents.RequestRateTooLargeException:消息:{"错误":["请求率很高"]}

目前我使用此代码限制请求。 WebJob函数:

public async Task ParseCategoriesFromCsv(...)
{
    double find = 2.23, add = 5.9, replace = 10.67;
    double requestCharge = Math.Round(find + Math.Max(add, replace));

    await categoryProvider.SaveCategories(requestCharge , categories);
}

操作文档数据库客户端的类别提供程序:

public async Task<ResourceResponse<Document>[]> SaveCategories(double requestCharge, Category[] categories)
{
    var requestDelay = TimeSpan.FromSeconds(60.0 / (collectionOptions.RequestUnits / requestCharge));

    var scheduler = new IntervalTaskScheduler(requestDelay, Scheduler.Default); // Rx

    var client = new DocumentClient(endpoint, authorizationKey,
        new ConnectionPolicy
        {
            ConnectionMode = documentDbOptions.ConnectionMode,
            ConnectionProtocol = documentDbOptions.ConnectionProtocol
        });

    return await Task.WhenAll(documents.Select(async d =>
       await scheduler.ScheduleTask(
           () => client.PutDocumentToDb(collectionOptions.CollectionLink, d.SearchIndex, d))));
}

任务调度程序来限制/测量/同步请求:

private readonly Subject<Action> _requests = new Subject<Action>();
private readonly IDisposable _observable;

public IntervalTaskScheduler(TimeSpan requestDelay, IScheduler scheduler)
{
    _observable = _requests.Select(i => Observable.Empty<Action>()
                                                  .Delay(requestDelay)
                                                  .StartWith(i))
                           .Concat()
                           .ObserveOn(scheduler)
                           .Subscribe(action => action());
}

public Task<T> ScheduleTask<T>(Func<Task<T>> request)
{
    var tcs = new TaskCompletionSource<T>();
    _requests.OnNext(async () =>
    {
        try
        {
            T result = await request();
            tcs.SetResult(result);
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    });
    return tcs.Task;
}

所以它基本上来自ResourceResponse<Document>.RequestCharge的一些常量,但是:

  • 当我有1个队列被触发时它工作正常但是当8个队列时它会抛出错误。
  • 如果增加请求费用8次,那么8个队列工作正常,但只有1个工作速度慢8倍。

什么节流/测量/同步机制可以在这里工作?

3 个答案:

答案 0 :(得分:2)

启动.NET SDK 1.8.0,我们会在合理范围内自动处理请求率过大的异常(默认情况下会重试9次,并且在从服务器返回后再次尝试重试)。

如果您需要更好的控制,可以在传递给DocumentClient对象的ConnectionPolicy实例上配置RetryOptions,我们将使用它覆盖默认的重试策略。

因此,您不再需要添加任何自定义逻辑来处理上述应用程序代码中的429个异常。

答案 1 :(得分:1)

当获得429(请求率太大)时,响应会告诉您等待多长时间。有一个标题x-ms-retry-after。这有价值。等待ms的那个时间段。

catch (AggregateException ex) when (ex.InnerException is DocumentClientException)
{
    DocumentClientException dce = (DocumentClientException)ex.InnerException;
    switch ((int)dce.StatusCode)
    {
        case 429:
            Thread.Sleep(dce.RetryAfter);
            break;

         default:
             Console.WriteLine("  Failed: {0}", ex.InnerException.Message);
             throw;
     }                    
}

答案 2 :(得分:1)

在我看来,您应该可以使用SaveCategories方法执行此操作,以使其与Rx很好地协同工作:

public IObservable<ResourceResponse<Document>[]> SaveCategories(double requestCharge, Category[] categories)
{
    var requestDelay = TimeSpan.FromSeconds(60.0 / (collectionOptions.RequestUnits / requestCharge));

    var client = new DocumentClient(endpoint, authorizationKey,
        new ConnectionPolicy
        {
            ConnectionMode = documentDbOptions.ConnectionMode,
            ConnectionProtocol = documentDbOptions.ConnectionProtocol
        });

    return
        Observable.Interval(requestDelay)
            .Zip(documents, (delay, doc) => doc)
            .SelectMany(doc => Observable.FromAsync(() => client.PutDocumentToDb(collectionOptions.CollectionLink, doc.SearchIndex, doc)))
            .ToArray();
}

这完全取消了您的IntervalTaskScheduler类,并确保您将请求率限制为requestDelay时间范围内的一个请求,但允许响应按需要进行。致.ToArray()调用将返回许多值的IObservable<ResourceResponse<Document>>转换为IObservable<ResourceResponse<Document>[]>,当observable完成时返回单个数组值。

我无法测试您的代码,因此我测试了一个我认为模拟您的代码的示例:

var r = new Random();
var a = Enumerable.Range(0, 1000);
var i = Observable.Interval(TimeSpan.FromSeconds(2.0));

var sw = Stopwatch.StartNew();

var query =
    i.Zip(a, (ii, aa) => aa)
        .SelectMany(aa => Observable.Start(() =>
        {
            var x = sw.Elapsed.TotalMilliseconds;
            Thread.Sleep(r.Next(0, 5000));
            return x;
        }))
        .Select(x => new
        {
            started = x,
            ended = sw.Elapsed.TotalMilliseconds
        });

我得到了这样的结果,表明请求受到限制:

 4026.2983  5259.7043 
 2030.1287  6940.2326 
 6027.0439  9664.1045 
 8027.9993 10207.0579 
10028.1762 12301.4746 
12028.3190 12711.4440 
14040.7972 17433.1964 
16040.9267 17574.5924 
18041.0529 19077.5545