Live data from Hacker News

A Brief Guide to OTP in Elixir

serokell.io

61–70 of 100 posts

Re: A Brief Guide to OTP in Elixir

#61
post #29
post #5

> In real life, we don’t need to write code with receive do loops. Instead, we use one of the behaviours created by people much smarter than us. I make more than half of my income from Elixir. That said, the naive receive loop is much easier to understand than any of the GenServer examples. They pollute the module logic with all that handle_* boilerplate. I believe that neither Erlang nor Elixir got the right abstrac…

What you said is rarely stated in public, but I've often felt it too: OTP obscures the underlying beauty of the Erlang platform. OTP is well engineered of course, but the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes. It's natural to think of case-specific abstractions around the primitives that are more germane to the…

I agree. The reason we stick with OTP, is that OTP behaviors like gen_server handle two system-level concerns that "raw" Erlang code doesn't:

1. OTP behaviors integrate with the OTP supervisor lifecycle management system (i.e. the OTP framework offers the supervisors standardized hooks to start up and shut down your process, guaranteeing that the errors generated during such steps will be in a format the supervisor can use);

2. Processes that implement OTP behaviors will react to `sys` messages; and so can be debugged, hibernated, code-upgraded, etc. on a framework level, "between" the times the process runs developer-defined code, without the developer having to write such handlers into their module.

This is all accomplished by passing control over the receive loop to a framework — `proc_lib` in this case — which in turn passes any messages it doesn't recognize back to your process. It's an inversion-of-control: proc_lib "is" your process; gen_server or whatever is a delegate of proc_lib; and your module is the delegate for gen_server. It's like an OOP class hierarchy in a GUI system, where the base class handles some events, subclasses handle others, and then your module only has to handle the few it's interested in.

But, if there was a way to do exactly that — to specialize one receive statement, defined in some other function, with your own additional clauses — then we wouldn't need the inversion-of-control framework of OTP! We could just write a regular receive statement that "points to" another receive statement as its "parent" or "fallback" (sort of like a chain of firewall rules, each pointing to the next as "what to check next if this one didn't handle it.") Ideally, the compiled result would be one fused receive statement that has all the clauses from all its parents.

And if you think about it, that's totally possible, even if Erlang itself doesn't offer a fancy chainable receive statement like that... because, in a language like Elixir tht has hygenic macros, you can use macros to generate such "fused receive statements"!

I'm honestly really surprised nobody has yet tried. I realized the possibility of this a couple years back, and have been waiting with baited breath for somebody to attempt it. If it worked out, this "tech" could be used to build a wholly-different-feeling language.

Re: A Brief Guide to OTP in Elixir

#62
post #55
post #53

IMO, Erlang/Elixir/BEAM/OTP really is the toolset that can be used to build what the mystical 10x/100x programmer can achieve. But that's also the reason why it's hard to push for its adoption into bigger teams. When the value proposition is "you can do all that by only using Erlang/Elixir/OTP...", while the alternatives are based on multiple, but more "traditional" and well-understood systems (which you are easier t…

The problem is that the runtime does too much magic and it's not portable / standard compared to other languages / runtimes. Lot of what BEAM/OTP is doing is done in other tools and it's language neutral. Not everything is good tbh, when you see the pain it is to deploy an Erlang app in 2020 ...

is it a pain?

mix release

Compiles everything, tar/gz's it, sends it to a private s3 bucket. On your server side, you periodically watch the s3 bucket, and when one of your nodes detects a relup, it downloads from the s3 bucket, kills itself. Systemd then restarts it, kicking into the newest version.

That's it. This is maybe about 50-100 lines of code, one external library (pick your AWS library of choice), and one systemd script. I think there's even a library for managing systemd from within the BEAM now.

If you want to be more sophisticated (a rolling blue-green deploy across an erlang cluster) you could probably do it with transactional locks with the :global module in about an additional 100-ish lines of code, including fully verifying the soundness of newly upgraded nodes using telemetry.

Re: A Brief Guide to OTP in Elixir

#63
post #8

Earlier quoted context omitted.

Would you mind linking to a couple of them? I don't know anything about Elixir (but am very comfortable in a lot of other languages), and am interested in the kind of thing more experienced people are using with Elixir. Thanks!

For web applications, Phoenix is very popular: https://github.com/phoenixframework/phoenix ; it uses some OTP directly and its dependencies like the web-server Cowboy use it as well. DB development usually uses Ecto: https://github.com/elixir-ecto/ecto , it has OTP servers for connection pooling and other tasks. For managing background tasks I use Honeydew: https://github.com/koudelka/honeydew

Your Phoenix link is broken (includes the semi-colon)

for the lazy: https://github.com/phoenixframework/phoenix

Re: A Brief Guide to OTP in Elixir

#64
post #3

OTP = Open Telecom Platform [1], though I think the full name is avoided nowadays. On an unrelated note, I've always been fascinated with the Erlang VM. The idea that you could hot-load modules and have multiple versions of the same module running at once seems really useful. I wonder why other runtimes haven't adopted these features? [1] https://en.wikipedia.org/wiki/Open_Telecom_Platform

Then they really need to pick a new name, because OTP is widely understood in programming to be a “One-Time Pad”, and without defining what they think it means in this context this is the least coherent article I’ve read in a while.

I never heard of your OTP, and I immediately knew which OTP they were talking about due to the Elixir keyword.

Re: A Brief Guide to OTP in Elixir

#65
post #3

OTP = Open Telecom Platform [1], though I think the full name is avoided nowadays. On an unrelated note, I've always been fascinated with the Erlang VM. The idea that you could hot-load modules and have multiple versions of the same module running at once seems really useful. I wonder why other runtimes haven't adopted these features? [1] https://en.wikipedia.org/wiki/Open_Telecom_Platform

OSGi (Open Services Gateway initiative) [1] provides this for the JVM by modularizing the ClassLoader [2]. If a module is upgraded but classes from the previous version are still referenced by other modules, both modules will run.

An OSGi "service" is not an actor but, like OTP with servers+supervisors, is the logical unit with which you build your systems. "bundles" (Java JARs with additional metadata) then act as the equivalent "application" that contain the services.

Outside of it being Java, I have strong opinions (positive and negative) that I've decided not to include.

[1] https://en.wikipedia.org/wiki/OSGi [2] https://en.wikipedia.org/wiki/Java_Classloader

Re: A Brief Guide to OTP in Elixir

#66
post #29
post #5

> In real life, we don’t need to write code with receive do loops. Instead, we use one of the behaviours created by people much smarter than us. I make more than half of my income from Elixir. That said, the naive receive loop is much easier to understand than any of the GenServer examples. They pollute the module logic with all that handle_* boilerplate. I believe that neither Erlang nor Elixir got the right abstrac…

What you said is rarely stated in public, but I've often felt it too: OTP obscures the underlying beauty of the Erlang platform. OTP is well engineered of course, but the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes. It's natural to think of case-specific abstractions around the primitives that are more germane to the…

> the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes

The frustrating thing is that when I made exactly such a tutorial two years ago, I was immediately chastised (on my old hosted commenting system) for showing something so low-level to start with:

https://alchemist.camp/episodes/simple-process-example

Re: A Brief Guide to OTP in Elixir

#67
post #29

Earlier quoted context omitted.

What you said is rarely stated in public, but I've often felt it too: OTP obscures the underlying beauty of the Erlang platform. OTP is well engineered of course, but the basic notion of the spawn -> receive -> loop cycle is so clean and illuminating that I wish newbies would hold out before learning OTP sometimes. It's natural to think of case-specific abstractions around the primitives that are more germane to the…

I finished reading Elixir in Action a few days ago. This was probably the best part of that book. It takes you through building a primitive GenServer with basic processes before moving onto actually using GenServer. It specifically goes over tail recursion optimization before it introduces the receive do loop, which seems like a very important part of the whole thing.

Elixir in Action is fantastic. Its approach is so different from the other introductory books, but the payoff is real.

Re: A Brief Guide to OTP in Elixir

#68
post #31
post #23

Earlier quoted context omitted.

I think these blog posts usually pretend it's simpler than it is. There's usually (almost always) durable storage required somewhere. But to respond to your example specifically, OTP offers quite a bit. To your points: Queues: a coordinator genserver process per node, which uses poolboy to only run X jobs at a time Tracking failures/sucesses: postgres Exponential backoff: a function Uniqueness across the cluster: std…

> There's usually (almost always) durable storage required somewhere. Despite the name, mnesia can provide durable storage, without using anything outside of OTP and your nodes' (hopefully durable) filesystems.

At WhatsApp we had a bunch of mnesia-backed services. I can't say that it was the best experience. Pretty much anything non-trivial, such as healing a cluster after a partition, or expanding a cluster, or cross-DC replication was a complete PITA to do properly. There's also very few people that have operational experience with it and very little documentation about it compared to pretty much any other storage engine.

The main benefit was the ability to load and store Erlang terms natively, along with the fact that it's just a library. The fact that your compute is colocated with your storage lends itself to a different architecture than when you use, say, a stateless web service in front of a relational database.

(Edit: I realize now that I'm replying to a WA old-timer so I'd be curious to know what your assessment of mnesia is with the benefit of hindsight.)

Re: A Brief Guide to OTP in Elixir

#69
post #24

Earlier quoted context omitted.

I use both kubernetes and OTP supervisors and don't find it misleading, rather I quite agree but maybe I'm missing something. I'm curious what you mean by that?

Interesting, thanks for the feedback. I find it misleading because Kubernetes is a massive platform that does all manner of things besides just supervising your Pods, and in fact I had to think about it for a while before concluding that the supervisory function is likely what the author was alluding to. It strikes me as saying that an oxygen mask in a hospital room is like an airplane: sure they both provide a facil…

I agree with your sentiment. I think a more apt metaphor than Kubernetes in its entirety would be comparing it to a Kubernetes ReplicaSet[1]. I can see some similarities there.

For fun, to expound on your analogy, maybe it's like comparing an airport to a hospital. They both feature a person you check in with (triage/ticket counter) to direct you to an appropriate room/gate depending on your needs, but the core mission of each are rather different.

[1] https://kubernetes.io/docs/concepts/workloads/controllers/re...

Re: A Brief Guide to OTP in Elixir

#70
post #3

OTP = Open Telecom Platform [1], though I think the full name is avoided nowadays. On an unrelated note, I've always been fascinated with the Erlang VM. The idea that you could hot-load modules and have multiple versions of the same module running at once seems really useful. I wonder why other runtimes haven't adopted these features? [1] https://en.wikipedia.org/wiki/Open_Telecom_Platform

Hot loading in Erlang is not magical. Here is an example. Let's say you want to redeploy some new code. In your deploy, you change some existing function foo in module A to call a new function bar in module B.

If you do this, you'd better make sure that you hotload the new version of B before you hotload the new version of A. Otherwise some process could end up trying to call B:bar through A:foo before the new B has finished loading.

If you rely on hotloading modules as your primary deploy mechanism, you will run into issues like this all the time. So it's not really clear to me that hotloading is a win for your typical stateless web service compared to just deploying a new binary and doing your standard zero-downtime deploy via forking.

It makes more sense when you realize that certain Erlang apps build up a huge amount of state in-memory. In those cases hotloading allows you to load new versions of the changed modules without having to take down the whole VM.

Post reply on HN