How can I use GenServer for external api request?

First of all. I have to say sorry that my question is not about code and might be vague.
But as I am studying GenServer and OTP section, one questions came up in my mind.
How can I use GenServer in my project?
In my application, I mostly use external api, such as Stripe and Twilio.
So I tried to use GenServer like this…

Client

  def send_sms(phone_numbers, body, from_number, account, token) do
    GenServer.cast(__MODULE__, {:send_sms, phone_numbers, body, from_number, account, token})
  end

Server

  def handle_cast({:send_sms, phone_numbers, body, from_number, account, token}, state) do
    Sms.send_sms(phone_numbers, body, from_number, account, token)
    {:noreply, state}
  end

But I don’t understand what the benefit is when I use GenServer here.
I want to get a response, so I have to use GenServer.call instead of cast.
what if I want to send 10000 sms using GenServer.call? GenServer,call is synchronous call and wait for response, is it correct using GenServer.call for this case?

I don’t think I fully understand a concept, benefit of using GenServer, and haveing trouble understanding it and adopting it for my use case.
Please help!

2 Likes

So in nutshell: GenServer is probably not something you want to use to make HTTP requests from your app.

Think of a GenServer as a process, which is - just like any other process - sequential in it’s nature. It does things one step at a time and only one thing inside GenServer happens at given time. It has also internal state that it maintains, and it can change it freely, and also maintains a queue of incoming messages.

When you do GenServer.cast, you put the message on top of that queue of incoming messages. When all other messages already in the queue are processed, your message will be processed. But your calling process will not get nor wait for any reply, it will continue it’s work immediatelly after sending message to GenServer.

When you do GenServer.cast, you are also putting a message on top of the queue. It will also be processed only after all other messages in the queue are processed. But in this case, your calling process pauses and waits until receives a reply message from GenServer. So it will block until all messages in queue have been processed, including it’s own.

And remember, GenServer does one thing at a time. So if you make HTTP calls from inside GenServer, it will block and make your calling processes wait. And if GenServer processes one message at time, it means that at any point in time your app will execute one HTTP call and one HTTP call only.

GenServer is a means of limiting concurrency in that case, and it very well might be something you need. For example, one could build a rate limiting HTTP client using GenServer(s). If you have some API limit, of saying 5 HTTP requests per second, a GenServer could act as a guardian enforcing that limit is not being exceeded.

But in your case, most likely you just want to make HTTP calls from the processes themselves, using dedicated background processing library or using simple Task that kicks off separate SMS sending process, possibly with help of TaskSupervisor to limit the concurrency to something like 20 concurrent HTTP calls so you don’t kill the service you use with your actions :).

6 Likes

Task is the ideal thing you can use for this purpose. You can invoke Tasks asynchronously and wait for their result.Task lives for the duration of the sending one SMS. On the other hand Genserver is like an object with state and takes your commands and queries. Commands -> Mutations -> cast, Queries -> Access state -> calls. While Task can be used with a state you have to handle the interaction by sending messages (primitive way). For Genserver, you dont have to send a message. The client API and server API are written in a single module. At first it is confusing. But think interms of running functions then you will know that these functions run in 2 different processes. Genserver encapsulates the sending / Receiving messages.

3 Likes

This might be a dumb question (I’m still learning), but isn’t a Task a GenServer “under the hood”?

So you could could spawn one GenServer for each HTTP call which would be the same as running a Task for each?

Or maybe I’m mistaken?

Nope.

http://erlang.org/doc/man/proc_lib.html#spawn-1

The idea with GenServer is that it has the capability to communicate with other processes that conform to OTP.

Note that there is a wrapper noreply/reply. The spawn doesn’t run the function directly but the wrapper does and the reply wrapper simply sends the result of the function back to the process that created the task in the first place.

6 Likes

That is a common mistake however. People assume Task is implemented as GenServer, and it’s simple wrapper on top of process instead.

2 Likes

Thanks @hubertlepicki and @peerreynders for clearing that up. I am glad I asked now!

Is there any disadvantage of using a GenServer in this scenario instead of a Task, other than a GenServer is overkill?

I wrote a little project for fun not too long ago where I scraped multiple URLs and I did this by using a DynamicSupervisor to start multiple GenServers. Perhaps I should have used Tasks instead?

That it is an overkill should be a reason enough not to use it. :wink:

I think DynamicSupervisor is a good fit for a scraper. They need to be massively parallel. I’d even go as far as to actually generate actions – like “find all links on url X” and “visit link Y and download its content” and “store the contents of link Y to this data storage” etc. – and put them in a queue so as to never lose a single scraper action. But that’s of course up to you. My point is, Task would be a poor fit for a scraper IMO.

(EDIT: …but it can still work pretty well if all the Tasks you are spawning are persistent.)

1 Like

But in your case, most likely you just want to make HTTP calls from the processes themselves, using dedicated background processing library or using simple Task that kicks off separate SMS sending process, possibly with help of TaskSupervisor to limit the concurrency to something like 20 concurrent HTTP calls so you don’t kill the service you use with your actions :).

Can you give me more information about ’ limit the concurrency ’ using Task.Supervisor?
any documentation links?

Thanks!

https://hexdocs.pm/elixir/Task.Supervisor.html#async_stream/6-options

Task.Supervisor.async_stream/6 will run the same function on each element of the supplied enumerable but on no more than :max_concurrency at the same time.

2 Likes

Sorry to bug you with another question but I think I’m confused so just wanted to clear it up.

Are you saying that using a DynamicSupervisor to start multiple child GenServers and each of those GenServers:

  • Download the HTML from a URL.
  • Parse the data.

Is a good solution? Or were you thinking having the multiple GenServers to only do the parsing of the HTML and leave the receiving of the HTML to tasks?

Thanks!

More like:

  • 1 traditional Supervisor supervising 2 DynamicSupervisors.
  • DynamicSupervisor 1: spawns processes or Tasks that download data from URLs.
  • DynamicSupervisor 2: spawns processes or Tasks that parse the data.
1 Like

You mean .call