0

I have a limit of 40 URL Session calls to my API per minute.

I have timed the number of calls in any 60s and when 40 calls have been reached I introduced sleep(x). Where x is 60 - seconds remaining before new minute start. This works fine and the calls don’t go over 40 in any given minute. However the limit is still exceeded as there might be more calls towards the end of the minute and more at the beginning of the next 60s count. Resulting in an API error.

I could add a:

usleep(x)

Where x would be 60/40 in milliseconds. However as some large data returns take much longer than simple queries that are instant. This would increase the overall download time significantly.

Is there a way to track the actual rate to see by how much to slow the function down?

Paul
  • 183
  • 1
  • 12
  • Why add so much complexity when you can just return instead of making more API calls till the next cycle starts? Start timer, reset number of calls, before making an API call check elapsed time and number of calls, if number > 40, return, if elapsed time > 60 seconds, reset everything. – cora Aug 10 '22 at 17:00
  • That’s what I did but it doesn’t work, because the calls are measured in any 60s not my lapses 60 seconds. So if 39 calls happen in the last second time will lapse but exceed the calls on the next. – Paul Aug 11 '22 at 07:06

2 Answers2

0

Might not be the neatest approach, but it works perfectly. Simply storing the time of each call and comparing it to see if new calls can be made and if not, the delay required.

Using previously suggested approach of delay before each API call of 60/40 = 1.5s (Minute / CallsPerMinute), as each call takes a different time to produce response, total time taken to make 500 calls was 15min 22s. Using the below approach time taken: 11min 52s as no unnecessary delay has been introduced.

Call before each API Request:

API.calls.addCall()

Call in function before executing new API task:

let limit = API.calls.isOverLimit()
                
if limit.isOver {
    sleep(limit.waitTime)
}

Background Support Code:

var globalApiCalls: [Date] = []

public class API {

let limitePerMinute = 40 // Set API limit per minute
let margin = 2 // Margin in case you issue more than one request at a time

static let calls = API()

func addCall() {
    globalApiCalls.append(Date())
}

func isOverLimit() -> (isOver: Bool, waitTime: UInt32)
{
    let callInLast60s = globalApiCalls.filter({ $0 > date60sAgo() })
    
    if callInLast60s.count > limitePerMinute - margin {
        if let firstCallInSequence = callInLast60s.sorted(by: { $0 > $1 }).dropLast(2).last {
            let seconds = Date().timeIntervalSince1970 - firstCallInSequence.timeIntervalSince1970
            if seconds < 60 { return (true, UInt32(60 + margin) - UInt32(seconds.rounded(.up))) }
        }
    }
    return (false, 0)
}

private func date60sAgo() -> Date
{
    var dayComponent = DateComponents(); dayComponent.second = -60
    return Calendar.current.date(byAdding: dayComponent, to: Date())!
}
}
Paul
  • 183
  • 1
  • 12
-1

Instead of using sleep have a counter. You can do this with a Semaphore (it is a counter for threads, on x amount of threads allowed at a time).

So if you only allow 40 threads at a time you will never have more. New threads will be blocked. This is much more efficient than calling sleep because it will interactively account for long calls and short calls.

The trick here is that you would call a function like this every sixty second. That would make a new semaphore every minute that would only allow 40 calls. Each semaphore would not affect one another but only it's own threads.

func uploadImages() {
    let uploadQueue = DispatchQueue.global(qos: .userInitiated)
    let uploadGroup = DispatchGroup()
    let uploadSemaphore = DispatchSemaphore(value: 40)

    uploadQueue.async(group: uploadGroup) { [weak self] in
        guard let self = self else { return }

        for (_, image) in images.enumerated() {
            uploadGroup.enter()
            uploadSemaphore.wait()
            self.callAPIUploadImage(image: image) { (success, error) in
                uploadGroup.leave()
                uploadSemaphore.signal()
            }
        }
    }

    uploadGroup.notify(queue: .main) {
        // completion
    }
}
  • That is what I am using in the main Task Manager. However as let’s say the first calls might take longer then next 30 calls happen at the same time. When new 60s cycle starts it goes over the limit as it hits more than 40 calls since first call. Even though less than in given 60s. That’s why I’m looking for a more complex idea to track the ratio based on last call and furtherst call. – Paul Aug 11 '22 at 07:05
  • I guess I am a bit confused. If you are required 40 calls per 60 seconds. Then after 10 minutes you should be allowed 400 calls and at 11 minutes 440 calls and ect... Or are you saying you are only ever allowed 40 calls total? In either case semaphores should be able to save the day for you. – Jon_the_developer Aug 11 '22 at 15:59
  • I’m already using semaphores for the task session. It doesn’t solve the problem, the API tracks any 60s not the 60s. I could make 5 calls in 59 seconds the 35 calls in 1 second. On the next 60s within 5 calls the api is exceeded. – Paul Aug 12 '22 at 05:09