[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)