Una forma sencilla de calificar las solicitudes de HttpClient

Estoy usando el HTTPClient en System.Net.Http para hacer solicitudes contra una API. La API está limitada a 10 solicitudes por segundo.

Mi código es más o menos así:

List tasks = new List(); items..Select(i => tasks.Add(ProcessItem(i)); try { await Task.WhenAll(taskList.ToArray()); } catch (Exception ex) { } 

El método ProcessItem hace algunas cosas pero siempre llama a la API utilizando lo siguiente: await SendRequestAsync(..blah) . Que se parece a

 private async Task SendRequestAsync(HttpRequestMessage request, CancellationToken token) { token.ThrowIfCancellationRequested(); var response = await HttpClient .SendAsync(request: request, cancellationToken: token).ConfigureAwait(continueOnCapturedContext: false); token.ThrowIfCancellationRequested(); return await Response.BuildResponse(response); } 

Originalmente, el código funcionaba bien, pero cuando comencé a usar Task.WhenAll empecé a recibir mensajes de ‘Rate Limit Exceeded’ de la API. ¿Cómo puedo limitar la tasa a la que se realizan las solicitudes?

Vale la pena señalar que ProcessItem puede realizar entre 1 y 4 llamadas a la API según el elemento.

La API está limitada a 10 solicitudes por segundo.

Luego simplemente haga que su código realice un lote de 10 solicitudes, asegurándose de que tomen al menos un segundo:

 Items[] items = ...; int index = 0; while (index < items.Length) { var timer = Task.Delay(TimeSpan.FromSeconds(1.2)); // ".2" to make sure var tasks = items.Skip(index).Take(10).Select(i => ProcessItemsAsync(i)); var tasksAndTimer = tasks.Concat(new[] { timer }); await Task.WhenAll(tasksAndTimer); index += 10; } 

Actualizar

El método My ProcessItems realiza de 1 a 4 llamadas a la API según el elemento.

En este caso, el procesamiento por lotes no es una solución adecuada. Debe limitar un método asíncrono a un número determinado, lo que implica un SemaphoreSlim . La parte difícil es que quieres permitir más llamadas a lo largo del tiempo .

No he probado este código, pero la idea general con la que me gustaría ir es tener una función periódica que libere el semáforo hasta 10 veces. Entonces, algo como esto:

 private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(10); private async Task ThrottledSendRequestAsync(HttpRequestMessage request, CancellationToken token) { await _semaphore.WaitAsync(token); return await SendRequestAsync(request, token); } private async Task PeriodicallyReleaseAsync(Task stop) { while (true) { var timer = Task.Delay(TimeSpan.FromSeconds(1.2)); if (await Task.WhenAny(timer, stop) == stop) return; // Release the semaphore at most 10 times. for (int i = 0; i != 10; ++i) { try { _semaphore.Release(); } catch (SemaphoreFullException) { break; } } } } 

Uso:

 // Start the periodic task, with a signal that we can use to stop it. var stop = new TaskCompletionSource(); var periodicTask = PeriodicallyReleaseAsync(stop.Task); // Wait for all item processing. await Task.WhenAll(taskList); // Stop the periodic task. stop.SetResult(null); await periodicTask; 

RESPUESTA ACTUALIZADA

El método My ProcessItems realiza de 1 a 4 llamadas a la API según el elemento. Así que con un tamaño de lote de 10 sigo superando el límite de velocidad.

Necesita implementar una ventana móvil en SendRequestAsync. Una cola que contiene las marcas de tiempo de cada solicitud es una estructura de datos adecuada. Se eliminan las entradas con una marca de tiempo anterior a 10 segundos. Como sucede, hay una implementación como respuesta a una pregunta similar en SO.

RESPUESTA ORIGINAL

Aún puede ser útil para otros

Una forma sencilla de manejar esto es agrupar sus solicitudes en grupos de 10, ejecutarlas simultáneamente y luego esperar hasta que haya transcurrido un total de 10 segundos (si es que aún no lo ha hecho). Esto lo llevará directamente al límite de velocidad si el lote de solicitudes puede completarse en 10 segundos, pero no es óptimo si el lote de solicitudes lleva más tiempo. Eche un vistazo al método de extensión .Batch () en MoreLinq . El código se vería aproximadamente como

 foreach (var taskList in tasks.Batch(10)) { Stopwatch sw = Stopwatch.StartNew(); // From System.Diagnostics await Task.WhenAll(taskList.ToArray()); if (sw.Elapsed.TotalSeconds < 10.0) { // Calculate how long you still have to wait and sleep that long // You might want to wait 10.5 or 11 seconds just in case the rate // limiting on the other side isn't perfectly implemented } } 

La respuesta es similar a esta .

En lugar de usar una lista de tareas y WhenAll , use Parallel.ForEach y use ParallelOptions para limitar el número de tareas concurrentes a 10, y asegúrese de que cada una tome al menos 1 segundo:

 Parallel.ForEach( items, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async item => { ProcessItems(item); await Task.Delay(1000); } ); 

O si desea asegurarse de que cada elemento se lleve lo más cerca posible de 1 segundo:

 Parallel.ForEach( searches, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async item => { var watch = new Stopwatch(); watch.Start(); ProcessItems(item); watch.Stop(); if (watch.ElapsedMilliseconds < 1000) await Task.Delay((int)(1000 - watch.ElapsedMilliseconds)); } ); 

O:

 Parallel.ForEach( searches, new ParallelOptions { MaxDegreeOfParallelism = 10 }, async item => { await Task.WhenAll( Task.Delay(1000), Task.Run(() => { ProcessItems(item); }) ); } );