[HN Gopher] Elixir GenServer Explained
       ___________________________________________________________________
        
       Elixir GenServer Explained
        
       Author : cheeseblubber
       Score  : 147 points
       Date   : 2021-04-27 17:15 UTC (5 hours ago)
        
 (HTM) web link (papercups.io)
 (TXT) w3m dump (papercups.io)
        
       | intergalplan wrote:
       | Maybe a dumb complaint, but I _hate_ the name. I have a really
       | hard time not reading it as the active name  "Generate Server".
       | Like, I'd expect it to be a function that generates servers
       | (whatever _that_ means, since what it 's dealing with isn't
       | something I'd normally call "a server" either). It bugs the hell
       | out of me every time I read Elixir code. One of those things I
       | have to keep reminding myself: "OK, it's not what it looks like
       | it is, at all... what did these misleading terms _actually_ mean,
       | again? "
        
         | sbuttgereit wrote:
         | When I first came to Elixir & Erlang, their naming convention
         | gave me no trouble at all. Now of course, I haven't done much
         | for decades with languages that would have given me the mental
         | model that would bias me to automatically be thinking of the
         | things you name. I do know that there are certain well-worn
         | development environments where you might gain such a bias.
         | 
         | Remember that Erlang, too, and some of these technologies,
         | names, and conventions, are pretty old and that they may not
         | have been as evocative then as they might be today with some of
         | their conventions. "Process" almost certainly would have been
         | confusing to the neophyte, but appropriately descriptive
         | nonetheless, but "Gen" for Generic.... eh.
         | 
         | Anyway I guess the point is that I have to look inward to see
         | if something like this rubbing me the wrong way is the
         | substandard choice of the project I'm diving into, or if it's
         | me bringing unwarranted biases and assumptions to the table.
        
         | dudul wrote:
         | I think it is accepted in the Erlang/Elixir community that some
         | names are pretty poor yeah. GenServer, OTP, Application. Even
         | Supervisor to a degree, I've read a post making the case that
         | it's more a lifecycle manager than a supervisor.
         | 
         | Anyway, it's just about getting used to the terms and what they
         | truly mean.
        
         | pessimizer wrote:
         | Is "general" or "generic" more misleading than "generate"?
        
           | [deleted]
        
         | brobinson wrote:
         | It's "generic" server:
         | http://erlang.org/doc/man/gen_server.html
        
           | hinkley wrote:
           | The sooner you start smacking people with tribal knowledge,
           | the sooner many people start to back away slowly.
           | 
           | I used to interview ex-coworkers. I don't do it as much
           | anymore because it gets old/depressing and the answers tend
           | not to change that much.
           | 
           | If, for the purposes of thinking about turnover, you look at
           | a former colleague as an 'aggrieved party', we are generally
           | somewhere in the neighborhood of mediocre at agreeing that
           | certain things were the 'final straw', and work backward
           | through that to actions to increase retention.
           | 
           | One of the things that interested me about these
           | conversations was that people tend to work chronologically
           | backward through many of their complaints. But what was
           | surprising was that some people would go all the way back to
           | their first weeks. To the _first_ straws. The warning signs
           | they ignored. In a way this is not unlike talking to someone
           | who just broke up with a romantic partner.
           | 
           | And while their ex may realize that his/her problems started
           | back with some early inconsiderate behavior that snowballed,
           | and resolve to 'do better next time', I rarely see companies
           | do this, unless I instigate it.
           | 
           | Point is, these petty slights stack up, and can become a big
           | part of someone's narrative for abandoning you (often in a
           | huff). First impressions are important, and you dismiss them
           | at your own peril.
        
             | dasil003 wrote:
             | What? GP's response is terse but not impolite, and
             | explaining what an abbreviation means is a far cry from
             | "smacking people with tribal knowledge", it's just table
             | stakes explanation in a field which requires precision.
             | 
             | The only reason modern computing got where it is today is
             | by standing on the shoulders of giants. If we go back and
             | re-litigate every naming decision of the past several
             | decades just to make newcomers feel welcome, all progress
             | will slow as we collectively devolve into continuous
             | bikeshedding.
        
               | hinkley wrote:
               | I'm talking about the name GenServer, not brobinson's
               | response.
               | 
               | There is the corollary:                   If we have not
               | seen farther, it is because giants were standing on our
               | toes.
               | 
               | There's a huge degree of cognitive dissonance on these
               | issues. Just because something had a reason in the past
               | doesn't mean we have to keep doing it forever. Also
               | "nobody understands" why people keep re-inventing wheels.
               | It's not all hubris, at least not all the time. It's also
               | declaring open season on those past compromises,
               | especially the ones people bump into immediately.
               | Especially the ones the current maintainers immediately
               | get defensive about. Technology dies for a lot of
               | reasons, but the apologists never seem to grasp that the
               | apologies are a stopgap. They explain the pain, they
               | don't cure it.
               | 
               | Believe it or not, I'm pro-Elixir. It's just that as I
               | learn any new technology, I start filling out proverbial
               | bingo card boxes for the things I can predict the next
               | person will complain about. Is that a bit cynical? It
               | could be, but it also serves as my todo list for when
               | someone makes an offhand comment about how you really
               | don't have to do this dumb thing, you could use this
               | library that does it for you. Now my coworkers either
               | don't have to worry about that or I have a suggestion
               | when they do.
               | 
               | Turns out when people are in pain, they appreciate
               | sympathy a lot more than they appreciate being gaslit
               | about how it's just their poor sense of history. Their
               | blatantly implied selfishness.
               | 
               | GenServer could use a new name. I think we forget
               | sometimes that you can rename old things by giving them a
               | second, better name and phasing out the old one. I don't
               | think there's anything seditious in that statement.
        
               | pmontra wrote:
               | I've been using Elixir for 4 years, it pays my bills and
               | I generally like it. GenServer could be a misleading name
               | (it never occurred to me) but the real mess is the bag of
               | handle this / handle that function names. They follow the
               | conventions of Erlang [1] but Elixir's developers could
               | have cast some syntactic sugar on top of it. The real
               | name of those functions is in their first argument.
               | handle_* is mostly noise.
               | 
               | [1] https://erlang.org/doc/man/gen_server.html
        
               | dnautics wrote:
               | if you don't mind a bit of boilerplate, I use this
               | pattern, it avoids syntactic sugar and achieves what you
               | are looking for:
               | 
               | https://www.youtube.com/watch?v=HA4h0cajgaA
        
               | supernintendo wrote:
               | Syntactic sugar is of course another form of abstraction
               | and so you have to balance developer ergonomics with that
               | added complexity. As you point out, Elixir's `GenServer`
               | is a fairly simple wrapper module over Erlang's own
               | `gen_server`. As new versions of OTP are released, the
               | maintainers of the Elixir language must also update their
               | own abstractions over those underlying Erlang libraries.
               | It also creates a niche disparity between Erlang and
               | Elixir which some Erlang developers might find
               | superfluous. Coming from Erlang, it's easy to appreciate
               | the syntax of Elixir even if it means getting over some
               | old habits. Perhaps changing the names of foundational
               | parts of the standard library would be less appreciated
               | by those already familiar with OTP, and feel as if the
               | mental overhead is being shifted to those developers
               | rather than simply being ameliorated for developers who
               | are new to the platform.
               | 
               | Designing a programming language is hard, especially when
               | building on top of a 35 year old language like Erlang. As
               | is often the case: if engineers could change the past
               | without breaking everything in the present, we would have
               | already done it. :)
        
               | intergalplan wrote:
               | FWIW I thought the response was entirely fine and it
               | wouldn't have occurred me to take offense at it.
               | 
               | (I actually googled what "gen" in "GenServer" was
               | _supposed_ to mean before posting, because I couldn 't
               | remember but knew it wasn't "generate", and couldn't find
               | an answer, including in the docs for GenServer)
               | 
               | [EDIT] to clarify, I failed to find the answer in any of
               | the _Elixir_ docs for GenServer. Evidently I should have
               | looked at Erlang.
        
               | dnautics wrote:
               | Version 1.0.4, first paragraph:
               | 
               | https://hexdocs.pm/elixir/1.0.4/GenServer.html#content
               | 
               | `The advantage of using a generic server process
               | (GenServer)`
               | 
               | current release (1.11.4), first paragraph;
               | 
               | https://hexdocs.pm/elixir/1.11.4/GenServer.html#content
               | 
               | `The advantage of using a generic server process
               | (GenServer)`
        
               | intergalplan wrote:
               | TIL: ignore elixir-lang.org, go to hexdocs.
        
         | enraged_camel wrote:
         | GenServer is a (behavior) module name. IMO, it doesn't make a
         | lot of sense to expect it to mean "generate server". If it was
         | a gen_server() function then sure. Or a ServerGen module - that
         | generates servers.
        
           | intergalplan wrote:
           | The _name_ makes me read it as  "generate server". Nothing
           | else about it fits with that. That's the problem.
        
         | elsurudo wrote:
         | You got downvoted, but I agree. "Gen" is very typically used as
         | a shorthand for "generate", whereas I've _only_ seen it used as
         | a shorthand for "generic" in the Erlang/Elixir world.
        
           | intergalplan wrote:
           | Upvoted, then downvoted to hell, haha.
           | 
           | IDK, I don't live in Elixir and that name trips me up every
           | damn time I need to read/write some. Everywhere else, "gen"
           | is typically a shortening of "generate" (which I don't love
           | either--just write the word--but it's fairly common), and
           | "generic" rarely occurs in code at all (elsewhere related to
           | programming, yes--in code, no)
           | 
           | [EDIT] incidentally, as long as I'm complaining about Elixir,
           | I've done a lot of Ruby and have _no_ clue whatsoever why
           | people act like Elixir is similar to it, yet constantly see
           | "oh yeah, it's so easy for Rubyists because it's so similar".
           | Then again, I haven't done any Phoenix with Elixir, so maybe
           | they just mean Phoenix is Rails-like.
        
           | dnautics wrote:
           | I have sympathy for gp. But also, naming things is hard. Wait
           | till you dig deeper into the erlang rabbit hole and discover
           | the undocumented gen module.
        
             | hazn wrote:
             | And in recent computer science (~30 years) we often pick
             | unintuitive names for things.
             | 
             | Examples: WebAssembly(neither web, nor assembly),
             | Serverless (has a server, actually), JavaScript.
        
       | Zarathu wrote:
       | > In particular, I was itching to learn more about handling
       | concurrency in Elixir. This, of course, led me to GenServers.
       | 
       | Might be a nitpicking here, but GenServers aren't useful for
       | concurrency. They just manage state, and only process one message
       | at a time. If you're using this as a cache, your reads will be
       | bottlenecked by however quickly the GenServer can handle the read
       | requests.
        
         | Philip-J-Fry wrote:
         | If you've got 5 gen_servers and you cast each one a message
         | then that is concurrency. The whole point of a gen_server is
         | that it's a separate process doing it's own thing.
         | 
         | I can dump 1000s of things into its message queue and then do
         | something else. It'll keep working away.
         | 
         | It's like saying threads aren't useful for concurrency because
         | they can only do one thing at a time.
        
         | supernintendo wrote:
         | > GenServers aren't useful for concurrency.
         | 
         | Sure, maybe not by themselves but typically you would run many
         | GenServers concurrently in your application as part of a
         | supervision tree. Libraries like Broadway (and the underlying
         | GenStage) are essentially just leveraging GenServer to make it
         | easier to orchestrate concurrency and state synchronization
         | across multiple processes in your application. But you could
         | build a comparable system on your own just using GenServer and
         | a dynamic supervisor.
        
       | areichert wrote:
       | In case anyone was curious what I used for the diagrams in this
       | article, it was an awesome open source [0] tool called
       | Excalidraw! [1]
       | 
       | [0] https://github.com/excalidraw/excalidraw
       | 
       | [1] https://excalidraw.com/
        
         | muxator wrote:
         | Excalidraw is a gem. I discovered it weeks ago, and cannot but
         | praise it. The online instance is free and works really well,
         | plus you have the possibility of hosting your own copy and be
         | completely independent.
         | 
         | I also saw that there is the possibility of doing collaborative
         | real time drawing, but still did not try it.
         | 
         | I have seen that the exported SVGs could be simplified (lots of
         | repeated markup), and I am thinking about giving a try to do a
         | PR.
         | 
         | Excalidraw is superb.
        
       | davidw wrote:
       | This is also a good reference. Even if it's Erlang, what's going
       | on under the hood is the same:
       | 
       | https://learnyousomeerlang.com/clients-and-servers
        
       | jimbokun wrote:
       | Since these insert events are being buffered, what happens if the
       | process dies? Are all of those inserts lost?
       | 
       | I feel like Erlang/Elixir is designed to handle cases like this
       | robustly, but it's not clear to me how this code avoids losing
       | data when a process crashes, potentially up to 5s worth of
       | updates!
        
         | rozap wrote:
         | Yea, they're just in memory, so they're lost. Erlang/Elixir
         | doesn't solve this. There's no getting around the fact that you
         | need an ack that your event was processed and saved to some
         | durable store. A supervisor won't save you from lack of
         | acknowledgment at the app level. Any restart or retry logic
         | within beam itself is useless if a beaver chews through a power
         | cord and the machine halts. And what is an ack, anyway? Is an
         | ack from mongo as good as an ack from postgres? This is all
         | stuff the language can't know. These are all business
         | questions, all the language can do is give you tools handle
         | these cases, which Erlang/Elixir does.
        
         | dudul wrote:
         | Isn't it true of any, in-memory buffer implementation? It is
         | very easy to add some form of recovery/persistence to a
         | GenServer. You can have it write to a DB, ETS or disk. You lose
         | some throughput obviously, it's all about balance.
        
         | therein wrote:
         | Furthermore I have heard it is actually possible for casts to
         | be dropped under load. I am having a hard time confirming this,
         | and had a hard time confirming it back then too. And even
         | worse, I realize most of the time we code as if the casts will
         | be reliably delivered.
         | 
         | Can someone chime in on this?
        
           | toast0 wrote:
           | I don't think that's quite true, at least not of a gen_X
           | cast.
           | 
           | If you use erlang:send/3 with options or
           | erlang:send_nosuspend/2, you can have some cases where
           | messages would be dropped without trying. I think that may be
           | the source of the drop under load you're thinking of?
           | 
           | Without nosuspend, if you send to a node that's connected,
           | dist will queue it to be sent, but there's no guarantee it's
           | received because networking, and it may not even be sent if
           | the other side has gone away and the send queue is already
           | too large; There is a hard to express constraint that if your
           | message doesn't get received in this case, dist will
           | disconnect eventually, but it's important to note that the
           | tick time-out doesn't guarantee timely delivery either; as
           | long as some data is flowing, you can get quite a backlog; I
           | think I've gotten net_adm:ping times above 30 minutes in some
           | cases. Also, if the dist connection is dropped, but both
           | nodes are online, it will likely reconnect shortly, and some
           | messages will have been lost.
           | 
           | If you send to a node that's not connected, and didn't
           | specify no_connect, dist will queue the message while
           | attempting to connect, but if that attempt fails, the
           | messages will be dropped.
           | 
           | It's also possible for code running as a gen_server (or
           | GenServer, I suppose) to check how many messages are queued
           | for it, and run different logic. I've written gen_servers
           | that would drop optional requests if the queue was large.
           | Also, if client timeouts are known, there are ways to
           | approximate the time spent waiting in queue, and drop
           | requests if they are received after the client already timed
           | out; it's a little tricky to do this though.
        
           | dnautics wrote:
           | I think that casts are guaranteed delivery if the process
           | exists. Problem is, the caster can't know that with certainty
           | (there could be a race between checking, process death and
           | sending). Or the recipient could just ignore the cast
           | altogether. Out if the box it's impossible to know which
           | happened.
           | 
           | Calls by contrast check out a monitor on the counterparty and
           | crash with a timeout so you have guaranteed delivery and
           | acknowledgement, or crash the calling process.
           | 
           | A lot of times people from other plarforms rush to use casts
           | when they "don't need a response" but the actual meanings
           | have more to do with failure domains and rate limiting back
           | pressure; my personal feeling is you should default to call
           | and only use cast when you need failure isolation.
        
         | pmontra wrote:
         | It doesn't. We read data from the db in the init function when
         | the GenServer starts and make callers persist it before sending
         | it to the GenServer. Our GenServers keep data in memory as the
         | one in the post but we could read it from the db each time it
         | ticks to process a record.
        
         | [deleted]
        
         | edisonywh wrote:
         | The article touched on it briefly, but perhaps it wasn't clear.
         | 
         | If the process dies, the Supervisor invokes the `terminate/2`
         | callback so you're still able to process events.
         | 
         | Here are the relevant lines:
         | https://github.com/plausible/analytics/blob/b724def948d51a0f...
         | 
         | Keep in mind you need to trap exit signal to tell the
         | supervisor to invoke the callback, as done so here:
         | https://github.com/plausible/analytics/blob/b724def948d51a0f...
         | 
         | The erlang docs also mentions this:
         | https://erlang.org/doc/design_principles/gen_server_concepts...
        
           | brightball wrote:
           | I usually utilize this with an ETS cache to save and recover
           | GenServer state in the event of a crash.
        
         | lostcolony wrote:
         | Short Answer - yes, data will be lost. THAT SAID - this is true
         | of any buffering solution, it's why even the 'sync' style of
         | request can still time out.
         | 
         | Long Answer - What happens in any language where you batch
         | stuff in memory? If the system breaks down, you lose that data.
         | This isn't unique to Erlang/Elixir. There's a tradeoff that
         | persistent storage is slower than memory, but it's persistent.
         | So do you want to be fast, or do you want to be durable?
         | 
         | However, there is some flexibility to address this at a system
         | level. Namely, you wait for confirmation. A client, be that an
         | actual user, another process, etc, wants to know if the write
         | has been persisted. Whereas many other languages make this sort
         | of caching layer completely transparent to the client (i.e.,
         | they return success immediately, and so failures mean invisible
         | data loss as the cache is dropped), Erlang/Elixir's model makes
         | it so you HAVE to think about this. I sent a message to the
         | downstream process; do I care about a response? If I don't get
         | a response for any given interval, I don't know what happened
         | to that message. I can still do useful work in the meantime,
         | but I don't know that my message was fully handled (persisted).
         | I can retry or I can report failure or whatever.
         | 
         | This is true in any distributed system, and in Erlang/Elixir
         | it's expressed very evidently in the language constructs
         | (rather than being hidden from you).
         | 
         | You can build local disk caches if you want (in fact, there are
         | included tools to make this super easy for you; ETS, DETS, and
         | Mnesia allow you to shove Erlang terms into memory storage,
         | disk based storage, and a hybrid of the two with some nice DB-
         | like behaviors, respectively), but you need to choose to slow
         | your message ingestion to the speed of local disk writes in
         | that case (as well as handle synchronization of deletes in the
         | event of multiple writers to your downstream). An depending
         | what your upstream is, that doesn't provide a guarantee (i.e.,
         | a write to local disk != a persisted write from a user
         | perspective, because the disk could crash before it ever makes
         | it off the local one).
        
           | rjknight wrote:
           | > What happens in any language where you batch stuff in
           | memory? If the system breaks down, you lose that data. This
           | isn't unique to Erlang/Elixir.
           | 
           | A little over a decade ago I did a project using Erlang. My
           | next project was using Ruby, and I was suddenly _horrified_
           | by the fact that I had no idea what would happen if the
           | application crashed (and Ruby apps, at least in those days,
           | crashed fairly often). Erlang/Elixir both forces you to think
           | about these things, and gives you tools to address them,
           | where many other languages (or their libraries) simply assume
           | that we will stay on the happy path.
           | 
           | Of course, you can be very productive using languages that
           | just ignore the possibility of very rare failures. Many
           | successful systems work on the basis that sometimes shit just
           | happens and maybe some data does get lost. But, after
           | programming with Erlang or Elixir for a while, that situation
           | starts to feel less acceptable!
        
             | lostcolony wrote:
             | 100%
             | 
             | I consider the couple years I built systems in Erlang to be
             | fundamental for me. It's affected, for better and worse, my
             | entire approach to system design, at every level. It's
             | meant the stuff I or (now that I'm in management) my teams
             | tend to write is incredibly resilient (compared with the
             | other teams in the department), but also meant that I have
             | a really hard time with any Silicon Valley interview.
        
         | nrmitchi wrote:
         | I'd like a solid answer to this too; it's my biggest concern
         | with any sort of "batch online requests" functionality in _any_
         | language; unsafe shutdowns can /will always happen _at some
         | point_ , and it feels like this is always a data loss risk.
        
           | dnautics wrote:
           | To some degree there's always data loss risk... A backhoe
           | could cut a line, you could lose power to a PSU, rack, data
           | center, a meteor could destroy the willamette valley, etc.
           | What you want your system to provide you with is the
           | framework to understand the risks and what the recovery looks
           | like and when recovery is a lost cause.
        
         | tomjakubowski wrote:
         | I think you might solve this by writing a "reliable" supervisor
         | process which actually receives the messages, and preserves
         | message or state history to replay for its supervised process
         | in case it crashes. Of course this really just shoves the
         | problem up the stack, but at least you can stick to "let it
         | crash" when writing the supervised process, and take more care
         | when writing the supervisor which has a smaller scope of
         | responsibility.
        
         | karmajunkie wrote:
         | In this case, that's correct--you could lose up to 5s worth of
         | state, plus any buffered messages. That's by design here. The
         | business impact of that is going to depend on use-case, but I
         | personally would find it difficult to believe that there would
         | be any real impact in that event, _in this case_ , under high
         | load.
         | 
         | Obviously, different tolerances to that are going to
         | necessitate different designs. You can configure the size of
         | the message buffer when a GenServer starts, and if you have
         | very low tolerance for lost data, you'd want to use a
         | synchronous message (using call instead of cast, which blocks
         | the sender until it returns) and appropriate error handling.
         | 
         | BEAM has a plethora of features for reliable applications, you
         | just have to apply them appropriately.
        
         | paultannenbaum wrote:
         | You can add retry logic in your genServer, but for anything but
         | the most simple use cases, you would want to add a Supervisor
         | and define a retry strategy.
         | 
         | https://elixir-lang.org/getting-started/mix-otp/supervisor-a...
        
       | FrancoisBosun wrote:
       | Well explained article. Good job OP!
        
       | JohnCurran wrote:
       | This line in the opening paragraph really rubs me the wrong way:
       | 
       | > "I'm ashamed to say most of my Elixir education has been
       | through trial and error, figuring things out as I go along"
       | 
       | This attitude is so prevalent in software as if we are all
       | supposed to be divined with programming knowledge the moment our
       | IDE spins up. In every other industry that's exactly how you
       | learn: Get your hands dirty, make mistakes, and fix them.
       | 
       | There's a lot of things wrong with software engineering -
       | harboring this attitude that self-taught learning is bad or
       | shameful makes it unnecessarily worse
       | 
       | Great write-up otherwise
        
         | Jtsummers wrote:
         | There's a middle ground between (relatively) blind trial and
         | error and being divinely granted insight. Deliberate reading of
         | documentation (be it API, language standard, books, tutorials
         | (better ones), etc.) lets you learn without just trying things
         | or piecing a theory together from examples (underdocumented
         | ones that don't explain the why of their choices).
        
           | dnautics wrote:
           | you could also pay for education, attend workshops, code
           | projects on a team, etc.
        
         | [deleted]
        
         | vaer-k wrote:
         | Self-learning is one thing, but uninformed trial and error
         | based on a lack of research is another beast entirely.
        
         | StreamBright wrote:
         | Do you think that you can install a gas turbine the wrong way
         | start it up and fix it later?
        
           | selykg wrote:
           | Like most things, you build up to it.
           | 
           | You start software development with "Hello, world!" and then
           | you do more, like getting input from the user, storing it in
           | a variable, then eventually you're using classes and working
           | with objects, then you're tying together a bunch of classes
           | and working with APIs.
           | 
           | No one just "studied" how to create a gas turbine and wah-la,
           | it was made. The entire process of literally everything was
           | one learning exercise after another. We started by using
           | rocks as tools. Here we are, having built better tools from
           | experience and a lot of trial and error and fooling around
           | with things.
           | 
           | Hell, Lego are the same exact idea.
        
         | areichert wrote:
         | OP here -- I know what you mean. "Ashamed" is probably too
         | strong a word. I think the feeling I was trying to convey was
         | just that I'm not exactly an authority on Elixir, so take
         | everything I say with a grain of salt, and nitpick away :)
        
       ___________________________________________________________________
       (page generated 2021-04-27 23:00 UTC)