A naive rate limiter for API calls or database queries:
let scheduler = DispatchQueue.testScheduler
// 2 calls every 1 second
let strategy = QueueThroughputStrategy(rate: UInt(2), interval: .seconds(1), scheduler: scheduler)
var values: [Int] = []
(1...5).publisher
.rateLimited(by: strategy)
.sink(receiveValue: {
values.append($0)
})
.store(in: &cancellables)
(1...5).publisher
.rateLimited(by: strategy)
.sink(receiveValue: {
values.append($0)
})
.store(in: &cancellables)
scheduler.advance(by: .seconds(0))
// Rate limiter starts immediately, and throughputs events up to the initial capacity:
print(values) // [1,1]
// Now limiter waits for its interval to pass...
scheduler.advance(by: .seconds(1))
// ... to get another round of values
print(values) // [1,1, 2,2]
By default, Airtable's Public REST API has a rate limit of 5 requests per second per base. Let's describe the limit in terms of QueueThroughputStrategy
:
extension URLSession {
private static let strategy = QueueThroughputStrategy(
rate: UInt(5),
interval: .seconds(1),
scheduler: DispatchQueue(label: "com.airtable.network")
)
}
Now we can use it with sessions.dataTaskPublisher
:
extension URLSession {
public func listRecords(request: URLRequest) -> AnyPublisher<Data, Error> {
dataTaskPublisher(for: request)
.map(\.data)
.rateLimited(by: Self.strategy)
.receive(on: DispatchQueue.main)
}
}
One thing to notice here, .rateLimited
emits events on a provided scheduler. So, don't forget to reschedule the events.
Surely not. The implementation is a kinda soft limiter. Requests can be delayed in flight and went entirely out of sync with the rate limiter publisher.
Make sure to add a circuit breaker retry:
- Controlling the timing of a Combine pipeline | Swift by Sundell
- Retrying a network request with a delay in Combine | Donny Wals
Surely not. I've used it in a couple of my projects without consequences. The library is tested to some extent. Alas, it's extremely hard to write custom Publishers. So, beware of race conditions.