我有一个带有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
的一些常量,但是:
什么节流/测量/同步机制可以在这里工作?
答案 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