💾 Archived View for tilde.team › ~kemal › posts › 2022 › what-ive-learned-about-genserver.gmi captured on 2022-06-04 at 00:11:49. Gemini links have been rewritten to link to archived content

View Raw

More Information

-=-=-=-=-=-=-

What I've Learned About GenServer

GenServer is a generic server implementation that is included in the Elixir standard library. The purpose of the GenServer is to give Elixir users the ability to run a process that has some kind of internal state and perform actions on that state. This is accomplished through the use of a standard set of callback functions which both accept the state as an argument and return it afterwards. The Elixir language site's description of the GenServer and the Hexdocs page for it are included below.

https://elixir-lang.org/getting-started/mix-otp/genserver.html

https://hexdocs.pm/elixir/GenServer.html

I recently decided to work on a TUI client for Gemini and decided to use Elixir for it since it seemed like a good fit and a good learning opportunity. One of the first things I started using was the Erlang SSL library. Since Elixir allows near seamless interop with Erlang code, Elixir can leverage a lot of existing Erlang libraries, and the SSL library is a good example of one that makes more sense to reuse than to try and recreate in Elixir. So, I started using it based mostly off its documentation page, located below.

https://www.erlang.org/doc/man/ssl.html

Specifically, I followed the instructions on this page to send some example SSL requests to Gemini pages in an iex session.

https://www.erlang.org/doc/apps/ssl/using_ssl.html

While that got me up and running and allowed me to make some simple requests, I quickly ran into something I didn't understand when I tried to translate that into code in an actual program instead of an iex session. That problem was that I didn't really understand the messaging model here. The way this works in Elixir and Erlang is that one process sends SSL messages to a remote server, and that remote server responds (or doesn't) with messages that contain the content of the page one is requesting. But how can we access those messages? They are returned to the process that initiated the calls, but they aren't automatically printed to stdout or anything. In the example, the instructions say to `flush` the iex shell, which will make all the messages appear. What is the programmatic equivalent of that in code?

On researching that question, I encountered people saying to just use a GenServer for this. I'd never used a GenServer and didn't really understand how it was supposed to solve this problem. The docs are also difficult to understand if you don't have some underlying knowledge of Elixir/Erlang processes and the messaging format. Since the GenServer is an advanced topic, most of the guides kind of expect some advanced process knowledge. Unfortunately, I have none of that, and was trying to use a GenServer in a project when I had little knowledge of the underlying messaging pattern. But I was able to figure it out and thought it would be good to document it here.

So how the GenServer fits in is it basically gives you hooks that you can use to access those messages. That was what I needed. This code specifically does that.

@impl true
def handle_call({:connect, url}, _from, state) do
  {:reply, connect(url), state}
end

@impl true
def handle_info({:ssl, sslsocket, content}, state) do
  {:noreply, state <> parse_response(to_string(content))}
end

@impl true
def handle_info({:ssl_closed, sslsocket}, state) do
  IO.puts(state)
  {:noreply, state}
end

There are 3 functions here, each of which implements an interface from the GenServer. The first is a call. That is a synchronous operation (the calling code waits for a response) that calls some code I wrote (the function `connect`) and returns either an `:ok` or some error response. That is what sends the request to the remote server. Then there are 2 implementations of the `info` function. Each of them accepts a different set of parameters (they're overloaded) and returns different responses. The important thing that `handle_info` does is it responds to messages returned to the GenServer process! That's exactly what I wanted. So, I send a request to a remote server. Then, when that remote server sends back a message, I have 2 hooks here that do different things depending on what the message is. If it's an `:ssl` message with content, I process that content and append it to the `state` (just a String in this example). If it's an `:ssl_closed` message with no content, I instead print all the state I have so far. The way this all works together is when I send a request to a remote page, I get back some number `n` of messages. So the first `handle_info` function should get called `n` times. Each time it is, the `state` grows until it contains all the web page's content. Then I get one more message, this time an `:ssl_closed` message telling me the page has sent back all its content. So then I print out all the content to stdout. Pretty nice, huh? I got all that state handling and message responding for free from the GenServer. My only gripe here is that I haven't found a nice document that gives me a good example of using a GenServer for anything like this. I have seen examples involving pushing and popping from a list, but that doesn't seem like a sensible application of GenServer. My purpose was really to give me a way to hook into the messages received by my process, and I didn't see that explained anywhere. I kind of felt it out from the documentation.

Anyway, hope that's helpful to someone else exploring Elixir!

Posted on: 25/05/2022