GenStage and GenServer in the same module

Is it possible to use both genstage and genserver in the same module? Wouldn’t their callbacks clash?

defmodule Test do
  use GenServer
  use GenStage

  ...
end

Right now I have a genserver whose role is to control two ets tables. It did’t have any state, so I thought I would collect the updates to these tables in this genserver’s state so that it can be a producer for genstage.

The original problem is that I have a leaderboard which I want to slightly “decouple” from the web part of the app (phoenix). Both web and leaderboard are apps under umbrella. But I also want to be able to send updates through channels whenever the leaderboard changes.

My first attempt at solving this was to make Test.Endpoint.broadcast/3 calls from inside the leaderboard module, but that didn’t work for me because in this case the leaderboard was dependent on the web part, and i wanted it the other way around because I think it makes more sense. Also whenever someone joins the channel they get the current state of the leaderboard in response.

Now i’m trying to use genstage but i’m afraid it’s overkill for my problem.

1 Like

GenStage is a GenServer with additional behaviour, so I’m not sure I see what combining the two would give.

2 Likes

I don’t really know what’s the solution to your specific use case, but using multiple modules of this type inside your module will end with GenStage overriding some methods of GenServer.

That is because some common methods are defined differently on both, as you can see for examplenhere:

as @michalmuskala said, GenStage is a particular case of a GenServer, so maybe it could work, I don’t really know for sure.

Hope that helps :slight_smile:

2 Likes

Just replacing GenServer with GenStage worked, thanks. Still not sure if genstage is the way to go though.

1 Like

Actually, no, it didn’t work. Now i get the error which i don’t understand.

iex(3)> Leaderboard.get_top 10
** (exit) exited in: GenServer.call(Leaderboard, {:get_top, 10}, 5000)
    ** (EXIT) bad return value: {:reply, [], []}
[error] GenServer Leaderboard terminating
** (stop) bad return value: {:reply, [], []}
Last message: {:get_top, 10}
State: []
[error] GenServer #PID<0.830.0> terminating
** (stop) bad return value: {:reply, [], []}
Last message: {:DOWN, #Reference<0.0.3.2532>, :process, #PID<0.829.0>, {:bad_return_value, {:reply, [], []}}}
State: nil
    (elixir) lib/gen_server.ex:737: GenServer.call/3

Why is {:reply, [], []} a bad return value? It worked fine before with genserver instead of genstage.

The code that was called

def get_top(limit) do
  GenStage.call(__MODULE__, {:get_top, limit})
end

def handle_call({:get_top, limit}, _, state) when is_integer(limit) and limit > 0 do
  best = limit |> do_get_top() |> :lists.reverse
  {:reply, best, state}
end 

defp do_get_top(limit) do
  last = :ets.last(:leaderboard_score)
  do_get(limit - 1, last, [])
end

defp do_get(_n, :"$end_of_table", acc), do: acc
defp do_get(0, {score, user_id}, acc), do: [{user_id ,score} | acc]
defp do_get(n, {score, user_id} = last, acc) do
  prev = :ets.prev(:leaderboard_score, last)
  do_get(n - 1, prev, [{user_id, score} | acc])
end
1 Like

Oh, stupid me. With genstage it needs to be {:reply, reply, [event], new_state}.

2 Likes