universal_server() ->
receive
{become, F} ->
F()
end.
Honestly I don't fully appreciate the power of this universal server. Can anyone help? universal_server() ->
receive
{become, F} ->
F()
end.
Honestly I don't fully appreciate the power of this universal server. Can anyone help?What he is saying is that you can swap out the logic of this waiting loop with whatever protocol logic you want. His example was that he had a fleet of machines that were running this loop, and he sent all of them a function that implemented a gossip protocol. But he could easily send them all another message that turns them all into BitTorrent clients.
Joe was an absolute genius and an extremely kind person. I had the honor of meeting him once. Erlang is still one of the most beautiful technical creations I've ever encountered. It really does make you see concurrency in a whole new way.
Is there a concise way to explain how Erlang achieves this property?
Completely agree. Joe was always willing to reply to my emails and answer my questions in detail. He was an exceptionally talented explainer, and his messages were always interesting and entertaining, while also being informative.
I never met him in person but I will always treasure the correspondence I had with him.
“Make it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!”
When you open a file, a socket, allocate memory, borrow a database connection from a pool, etc. you don't need to write try/catch/finally, or defer() statements, or logging, or any kind of error handling like that - if your code crashes, the VM will take care of the cleanup (because it knows what resources are owned by your process) and the supervisor above your process will log the problem and restart the process if necessary.
This makes Erlang applications much safer by default, and the business logic is clearer and simpler because it's not mixed with error recovery code (the "if err != nil" every other line that Go is famous for). The error kernel (the part of the app that must be carefully written to ensure reliability) can be kept really small[1].
This comes at a cost in performance, because data must generally be copied between processes, whereas goroutines can send pointers directly to each other. But if you can afford it, it's _very_ nice. Besides the error handling this also enables crazy observability (you can connect to a running app and inspect processes[2], kill them, send them messages, jump between nodes in a cluster, etc.), live code reload, and a bunch of nice things like that.
[Process isolation also enables clustering, where processes running on different machines can talk to each other as if they were in the same OS process. The copying means it doesn't matter if the other process is remote or local.]
[1] https://medium.com/@jlouis666/error-kernels-9ad991200abd
It's not an accident that large distributed chat programs or MQ systems are often written in erlang. From inside the code, writing a distributed system feels the same as working on one node.
With go, you can't do channels to a go process running on a different server without significantly changing the language. In erlang, you don't notice because that's just how it works.
Write an app that runs on two nodes and sends updates to all clients via websockets. In erlang, you just write it. In EVERY other language, you're loading a non-idiomatic library and probably running an MQ cluster or using an MQ service... probably written in erlang.
Here they made a new server that takes an return process (From) and a number (N). The exclamation point sends the result back to the return process.
factorial_server() ->
receive
{From, N} ->
From ! factorial(N),
factorial_server()
end.
factorial(0) -> 1;
factorial(N) -> N * factorial(N-1).
This code then spawns the server, sends a message to that server to become a factorial server, then tells that server to send it back a message with the factorial of 50. It then specifies it's own message listener that takes whatever it receives and returns it. test() ->
Pid = spawn(fun universal_server/0),
Pid ! {become, fun factorial_server/0},
Pid ! {self(), 50},
receive
X -> X
end.
A couple of the major advantages of Erlang its distributed parallel nature, and also hot code update. Which happens in `Pid ! {become, fun factorial_server/0}` where it overides the receive loop of universal_server with that of the factorial_server. Though I think proper hot code update doesn't work like thisBut with Erlang, you can have a distributed network of Erlang servers where the server is a generic computing resource that can do anything the client wants.
The code actually comes from the client. No need to get your system administrators to install some binary on all the machines. You simply pass the function along and the remote machine calls it.
I often wonder what would have happened if the "BEAM renaissance" (driven largely by the birth of Elixir and associated tools) had happened a decade earlier, before Kubernetes became the de-facto standard for ad-hoc distributed computing in web software.
It’s possible people are showing the capability of BEAM thou
Is pushing new code for your server to run "arbitrary code execution"? I guess we can call it that. Is it an exploit?
Depends if the code comes from some random person on the internet from mechanisms that you don't intend for pushing new code to run (e.g. through a buffer overflow on your server or XSS), or if it comes from yourself through your official mechanisms.
[1]: https://devblogs.microsoft.com/oldnewthing/20060508-22/?p=31...
Erlang’s architecture is unusual; both the virtual machine and the language are built around the idea of tiny processes operating concurrently, each process running in an infinite loop waiting for incoming messages to interpret.
This allows a process to become whatever code you send it. If you need a process to control a microwave, and then run some quantum computations, and then predict the winner of tomorrow’s football game, you just send it the code it needs for each operation and it happily does so.
f(10).
This will produce a result and return it to its caller. The other isn't really a call, it's "sending": Pid ! 10.
Some process id has been sent the value 10. It may be on this same node, it may be on another node, I don't have to care (sometimes I do though). This is asynchronous. Once a send is done the sending process will continue on (perhaps even terminating). At the other end of the send is a receive (hopefully, otherwise somebody's queue is getting filled up...): receive
N -> ... % do something with this value
end.
In the case of `universal_server` we don't know what it will become, it's just going to execute whatever 0-ary function is passed. That function may or may not include a "return" (sending a value back to the origin). It could also just terminate the universal server. Or it could temporarily convert the universal server into something else and then become a universal server again.And in his case he had access to some 9000 computers. If each was running at least one Erlang node and each node was running a universal server, then with a very simple program he could write a function, serialize the function, and distribute the function to his 9k+ running universal servers and turn them into 9k+ specialized servers.
As exotic as this sounds, this is very similar to what web-browsers do with script src tags, especially from 3rd parties. The page is saying "Hey let me eval a function that can do whatever it wants in this context. I trust you!" Most webdevs don't consider this a threat vector!
[0] https://gist.github.com/mndvns/80b00cf67d418e8359fb5566b80ae...