I'm querying an API which has a limit to the number of queries you can do a second. For example, you might be allowed to make 20 queries a second. If you over query the server you get a 429 error which prevents you from accessing the API.

The issue come with a foreach loop in my code where every iteration of the code requires a query of the API. Is there a way to code in the limit so that the foreach will only query a certain number of times within a set time limit so I do not reach the API's query limit? Or in other words, can I make my foreach loop iterate at a rate of 20 loops a second or any other number a second?

The foreach loop is below if you want to look at it but I do not believe you will need it to answer the question.

foreach(var item in matchlistd)
{
    var response2 = client.GetAsync([email protected]"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{item.summonerName}apikeyiswhatgoesintherestofthispartoftheapi).Result;
    if (response2.IsSuccessStatusCode)
    {
        var content2 = response2.Content.ReadAsStringAsync().Result;
        summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
        accountinfo.Add(player);
    }
}

3 Answers

0
Kevin On

Use a timer like below. You could also pass a variable for the timer interval.

private Timer _functionTimer; 

public void InitMatchList()
{
    _functionTimer_Tick = new Timer();
    _functionTimer_Tick.Tick += new EventHandler(_functionTimer_Tick);
    _functionTimer_Tick.Interval = 50; // in miliseconds
    _functionTimer_Tick.Start();
}

private void _functionTimer_Tick(object sender, EventArgs e)
{
    var response2 = client.GetAsync([email protected]"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{item.summonerName}apikeyiswhatgoesintherestofthispartoftheapi).Result;
    if (response2.IsSuccessStatusCode)
    {
        var content2 = response2.Content.ReadAsStringAsync().Result;
        summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
        accountinfo.Add(player);
    }
}
0
Erik Philips On

While a timer may work it's an old-school way of doing rate limiting. It may not work if the server does not want more than 20 conncurrent connections per second. Meaning if the result takes longer than the next timed-request you may still get a 429. A alternative solution might look like:

private static readonly httpClient = new HttpClient();
public async Task<IEnumerable<string>> GetAPIResults(IEnuemrable<MatchList> matchLists,
  int maximumRequestsPerSecond)
{
    var requests = matchLists
      .Select(ml => new RequestStatus { MatchList = ml })
      .ToList();

    foreach (var request in requests)
    {
      var activeRequests = RequestStatus
        .Where(rs => 
          (rs.RequestedOn.HasValue && rs.RequestedOn > DateTime.Now.AddSeconds(-1))
          || (rs.Task.HasValue && rs.Task.TaskStatus != TaskStatus.Running))
        .ToList();

      //wait for either a request to complete
      //or for a request not active within the last second to expire
      while (activeRequests > maximumRequestsPerSecond)
      {
        var lastActive = activeRequests.OrderBy(RequestedOn.Value).First();
        var waitFor = DateTime.Now - lastActive.RequestedOn.Value;

        // or maybe this to be safe
        // var waitFor = (DateTime.Now - lastActive.RequestedOn.Value)
        //   .Add(TimeSpan.FromMilliseconds(100));

        await Task.Delay(waitFor);

        activeRequests = RequestStatus
          .Where(rs => 
            (rs.RequestedOn.HasValue && rs.RequestedOn > DateTime.Now.AddSeconds(-1))
            || (rs.Task.HasValue && rs.Task.TaskStatus != TaskStatus.Running))
          .ToList();
    }

      request.RequestTask = httpClient.GetStringAsync(myUrl);       
    }

    await Task.WhenAll(requests.Select(r => r.RequestTask.Value));

    // not sure about .Result here...
    return requests.Select(r => r.RequestTask.Value.Result).ToList();

    // probably safer:
    return requests.Select(r => await r.RequestTask).ToList();
}

public class RequestStatus
{
  public MatchList MatchList { get; set; }
  public DateTime? RequestedOn { get; set }
  public Task<string>? RequestTask { get; set; }
}

The delay could be better if instead of just waiting for a specific amount of time, there was a Task.WhenAll() task that popped a CancellationToken for the Task.Delay() method.

1
Enigmativity On

You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive and add using System.Reactive.Linq; - then you can do some pretty cool stuff.

First off, we need to fix your code so that you don't have to rely on the .Result calls.

Let's just assume that your code is running in a method called Main - then you can change your code to work like this:

async void Main()
{
    // ...

    string BuildUrl(string summonerName) => [email protected]"https://na1.api.riotgames.com/lol/summoner/v4/summoners/by-name/{summonerName}apikeyiswhatgoesintherestofthispartoftheapi";

    foreach (var item in matchlistd)
    {
        var response2 = await client.GetAsync(BuildUrl(item.summonerName));
        if (response2.IsSuccessStatusCode)
        {
            var content2 = await response2.Content.ReadAsStringAsync();
            summonerName player = JsonConvert.DeserializeObject<summonerName>(content2);
            accountinfo.Add(player);
        }
    }

    // ...
}

Note the async and the two await keywords.

Now let's rewrite your loop to be an Rx observable. An observable is like an enumerable, but instead of producing all of the values immediately, it produces values one at a time.

IObservable<summonerName> query =
    /* 1 */ from item in matchlistd.ToObservable()
    /* 2 */ from response2 in Observable.FromAsync(() => client.GetAsync(BuildUrl(item.summonerName)))
    /* 3 */ where response2.IsSuccessStatusCode
    /* 4 */ from content2 in Observable.FromAsync(() => response2.Content.ReadAsStringAsync())
    /* 5 */ select JsonConvert.DeserializeObject<summonerName>(content2);
  1. turns your matchlistd enumerable into an observable
  2. calls client.GetAsync and unwraps the task using the Observable.FromAsync to create an observable of the message response
  3. filter out response2.IsSuccessStatusCode == false
  4. calls response2.Content.ReadAsStringAsync() and unwraps the task using the Observable.FromAsync to create an observable string
  5. converts the string to summonerName.

Then you can do this to get all of the results and put it in your list:

accountinfo.AddRange(await query.ToList());

Now we just want to make this only produce up to 20 queries per second. Here's the modified query:

IObservable<summonerName> query =
    from items in matchlistd.ToObservable().Buffer(20).Zip(Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)), (x, t) => x)
    from item in items
    from response2 in Observable.FromAsync(() => client.GetAsync(BuildUrl(item.summonerName)))
    where response2.IsSuccessStatusCode
    from content2 in Observable.FromAsync(() => response2.Content.ReadAsStringAsync())
    select JsonConvert.DeserializeObject<summonerName>(content2);

Note the following part:

    from items in matchlistd.ToObservable().Buffer(20).Zip(Observable.Timer(TimeSpan.Zero, TimeSpan.FromSeconds(1.0)), (x, t) => x)
    from item in items

That's the secret sauce. .Buffer(20) & TimeSpan.FromSeconds(1.0) are the bits you can change to customise the behaviour.