nezzart

nezzart

I’ve read on GenServer, but am not able yet to use it because not everything yet makes sense to me.

I want this:

  • when a user clicks on a button, a new task begins
  • a task terminates depending on some external condition, say, an external rest-api webservice has returned “terminate = true”
  • if a task is already running, clicking on a button shouldn’t begin a new
  • a task poll an external rest-api webservice every 30 seconds

I was adviced to use a single GenServer. My questions:

  1. how can I associate a new task or its name with a user’s id? so that when he clicks on a button I can check if I need to create a new task or do nothing if there’s already one running?

  2. how can I add/remove an item to MyWorker? Meaning, should I keep a list of users id?

  3. if there’re no tasks, GenServer should stop. I think. Is it a wise thing to do? If so, how can I stop/run it?

  4. to re-run a task every 30 seconds I need to call schedule_work() in “poll_external_service”, correct? Thus, to terminate it, depending on a response from an external web rest service, I merely don’t run schedule_work(), correct?

    defmodule MyWorker do
    use GenServer

     def start_link do
       GenServer.start_link(__MODULE__, [], name: :my_app_worker)
     end
    
     def init(state) do
       schedule_work() 
       {:ok, state}
     end
    
     def add_item(x) do
    
     end
    
     def remove_item(x) do
    
     end
    
    
     defp poll_external_service do
       # a request to an external web server
    
       schedule_work()
     end
    
     defp schedule_work() do
       Process.send_after(self(), :work, 30 * 1000) 
     end
    

    end

Showing Posts 1 to 10

net

net

I may have misunderstood what you’re trying to do, but this might help.

Assuming that you only wanted to store the user ID for each ask, I used a MapSet. If you want to store additional data with the task (such as a specific URL to poll), you should use a Map (with the key being the user ID, as map keys are unique).

In process_tasks/1 we use Enum.filter/2 to both call a function for each task, and to remove tasks that the external API has indicated should be terminated. The case ExternalAPI.poll(id) do is of course, a contrived example. What’s important is that the anonymous function given to Enum.filter/2 returns false if the task should be removed. (You can give Enum.filter/2 a map too, if you go that route. In that event the anonymous function should take a tuple {user_id, data} representing each key-value pair in the map.)

There’s no need to stop the GenServer when there are no tasks. Erlang processes are very lightweight.

defmodule MyWorker do
  use GenServer

  @tick_interval 30_000

  def start_link, do: GenServer.start_link(__MODULE__, nil, name: __MODULE__)

  def add(id), do: GenServer.cast(__MODULE__, {:add, id})

  def remove(id), do: GenServer.cast(__MODULE__, {:remove, id})

  def init(_) do
    tick()

    # Here we use a MapSet as it will reject duplicate items.
    # If you wish to store more data with a task than just a user ID,
    # use a Map instead.
    {:ok, MapSet.new}
  end

  defp tick, do: Process.send_after(self(), :tick, @tick_interval)

  def handle_cast({:add, id}, tasks), do: {:noreply, MapSet.put(tasks, id)}

  def handle_cast({:remove, id}, tasks), do: {:noreply, MapSet.delete(tasks, id)}

  def handle_info(:tick, tasks) do
    tasks = process_tasks(tasks)

    tick()

    {:noreply, tasks}
  end

  defp process_tasks(tasks) do
    tasks_list =
      Enum.filter tasks, fn(id) ->
        # Here we process the individual task, such as polling
        # the external API.
        case ExternalAPI.poll(id) do
          # We return true from this anonymous function if the task should
          # stay, and false if the task should be removed.
          "terminate = true" -> true
          "terminate = false" -> false
        end
      end

    # `Enum.filter/2` returns a list, so we must convert it back into a MapSet.
    MapSet.new(tasks_list)
  end

  def handle_info(_, tasks), do: {:noreply, tasks}
end
nezzart

nezzart OP

Thanks.

In you example In process_tasks a list of tasks is iterated over and an http request ExternalAPI.poll is sent syncronously and therefore waited for a response syncronously as well. Is that a wise thing to do? Why not create for each http request a separate thread?

net

net

You’re right, that is better. Instead of Enum.filter/2 you might consider just using Enum.each/2 and calling MyWorker.remove/1 from the process handling the individual task if the task should be removed.

nezzart

nezzart OP

Should that be done via spawn? something like this:

spawn(Helloer, :my_func, [123, "something"])

Then how would I do filtering if all them are in different thread? Will I have to wait something for all them for finish in each iteration?

net

net

I would use a Task.

As I mentioned, if you go that route you may not want to use Enum.filter/2. Instead, call the remove/1 function from inside the Task process with the user ID of the task to be removed.

Enum.each tasks, fn(id) ->
  Task.start fn ->
    if ExternalAPI.poll(id) == "terminate = true", do: remove(id)
  end
end
nezzart

nezzart OP

Also, how is an instance of MapSet stored between http requests? I mean, an http request to an html page: a user reloads a page with a button and … a list of tasks is gone? In a session or is there more idiomatic way?

net

net

I’m not entirely sure what you’re asking.

The MapSet is stored as the MyWorker GenServer’s state on the server. The MyWorker GenServer is a continuously running process that should be started as part of your application’s supervision tree when your application starts. You should not be starting and stopping it on demand when a user loads or reloads a page.

nezzart

nezzart OP

There’s no variable in MyWorker which stores MapSet. Where is it stored?

Will you show me a simple example of a client of MyWorker?

Qqwy

Qqwy

TypeCheck Core Team

The MyWorker uses the :name option as part of the start_link call, which means that the process is registered under a global (node-wide) name. The other calls that use GenServer.call and GenServer.cast use the same name to access it later.

So this means that the process will remain available after starting until your application closes, the process crashes, or you call GenServer.stop/2 from somewhere. In any case, the process will remain regardless of the connecting/disconnecting of users to your web-facing interface.

nezzart

nezzart OP

that’s not what I asked about.

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews