_jonas
Hey I’ve got a quick beginner question:
I’m making requests to an external API using Finch.
The API is rate limited, and when the limit is hit, it returns a 429 response which helpfully comes with a Retry-After header that declares a time in seconds after which the request can be retried.
My naive approach would be to route all the external API calls through a GenServer, which when encountering the rate limit, calls Process.sleep/1 on the value (*1000) returned by the Retry-After header, so that all subsequent requests are queued in the message queue of the GenServer process and successfully complete after the process finishes sleeping and the rate limit is lifted.
I am hesitating because of this line in the Process docs for sleep/1:
Use this function with extreme care. For almost all situations where you would use
sleep/1in Elixir, there is likely a more correct, faster and precise way of achieving the same with message passing.
Is this a situation where sleep/1 would be appropriate or is there indeed a better way?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
arcanemachine
I’m guessing by the lack of responses that this is one of those valid use cases.
From what I understand about the BEAM (which is not nearly enough…), making a process sleep like this is not a problem from an efficiency standpoint (i.e. the runtime can easily handle it).
I think that warning is to caution against naive time-based timeouts when there is a better way to do that. In this case however, it seems perfectly legitimate (to this newbie, at least) to do what you are doing since 1) there’s no more direct way of doing it, and 2) you know the exact amount of time you need to wait before performing the request again.
dwark
Wouldn’t that block the GenServer due to sleeping while handling a single API request?
Perhaps the GenServer could store the API call(s) to be delayed in a backlog (map) by some ID and use send_after to message itself to take API call with ID-x from the backlog and try again. That way, it would be able to handle incoming API calls instead of queueing them up in its message in-box. Assuming ofcourse those API calls are not all to the same end-point and/or do not fall under the same rate-limit…
arcanemachine
Cunningham’s Law saves the day again!
So is the GenServer needed at all? Or could it just be some function that calls itself after receiving a 429 and waiting for the timeout?
jswanner
I would not recommend using
Process.sleepfor this, if for nothing else there are bookkeeping tasks that are done under the hood that rely on message passing and you’re pausing all of that while sleeping.I would use
:queueto enqueue requests you want to have wait (storing thefromvalue and eventually replying withGenServer.reply/2). The calling processes will be blocked the whole time (or until they time out), but you don’t need the GenServer to be sleeping for thatdwark
Probably, but I am not sure I would recurse by calling myself again unless its with a flag that says that it’s a retry, in which case it either succeeds or fails. Otherwise you may end up recursing endlessly depending on the responses you get ..
_jonas
Thanks very much for the replies everyone!
Yes but that is my intention though, I was thinking of it like this:
This GenServer is responsible for making all calls for this particular API and nothing else. When the rate limit is hit, I do not want any new calls to be sent to this API until the
Retry-Aftertime has passed, so I explicitly want it to block.From my basic understanding of the BEAM this would mean no calls would be dropped though, instead they would pile up in the message inbox of the GenServer process, and when the process unblocks they will be processed, I feel like this is ideal for what I want to achieve.
Ah maybe that was a misunderstanding, all calls to this API fall under the same rate limit, the API rate limits based on my IP – therefore my wish to block and delay all calls when the rate limit is hit.
Ah this is interesting insight that I didn’t know / think about before, thank you.
Okay I understand this in principle I think, I’m just unsure how to implement waiting for the timeout returned by the header then.
After searching around a bit I found the Erlang
:timer.send_after, I suppose that would be what I want?So I would accept requests, always add them to a
:queue, process the:queueand if the limit is hit, stop processing the queue and set a flag in the GenServer state that no new requests can be accepted and send a message to the GenServer with:timer.send_afterwhich will trigger the flag to be set to false again and continue processing the messages again, correct?This is a fair amount of added complexity in my head at least, but if those GenServer bookkeeping tasks are important I suppose it would be better than sleeping.
I’m thinking the GenServer is needed because I want to delay any other calls to the same API for the duration specified by the
Retry-Afterheader, rather than just adding a delay to a single call. This is why I would want to create this bottleneck that would not let any new messages through to the API while waiting for the time to pass.dwark
Think @arcanemachine is right in that you probably do not need a GenServer for this.
Using something like:
in the part that handles the
Retry-Afterresponse would do the trick as well, no? Andcall_api(url, max_tries)would return an{:error, :max_tries}or something similar oncemax_triesreaches 0.Assuming you have only one caller that calls
call_api(url, max_tries)ofcourse._jonas
Hm, this is a bit difficult to parse for me as someone new to the language, let me see if I understand correctly:
You are using receive which is used for processing messages from the inbox of the current process, except there are no clauses in the receive block, so it will just block the process until the time specified in the after block has elapsed correct?
Is this not the same as
Process.sleepsince receive will block the process until the time has elapsed?But I understand that you are suggesting to introduce the waiting time in the caller, I don’t think this will serve me well since I want to coordinate the waiting between callers that could call from any process and have them all delayed until the rate limit is lifted.
I don’t want to drop or reject any calls under any circumstances, they should always just wait until the current rate limit is over.
No thats not a limitation I am willing to introduce, could be any number of callers from different processes.
dwark
Correct.
Well, if you have callers from different processes then you probably want to go with @jswanner’s advice.
_jonas
Great thank you!
So is this empty receive block thing with timeout a common pattern, and is it better than doing
Process.sleepor does it block the process in the same way?If I do this in my GenServer instead of
Process.sleep, would it allow the GenServer to still do its bookkeeping tasks?Sorry for all the questions, can’t really find any info on this pattern online.
For reference, here is my implementation with sleep which is called from inside the GenServer, works great so far but I am worried about the bookkeeping thing mentioned by @jswanner