Concurrency

Rubyists, have a sip of Elixir!

Rubyists, have a sip of Elixir!

by Benjamin Tan Wei Hao

In the talk "Rubyists, have a sip of Elixir!" by Benjamin Tan Wei Hao, presented at RubyConf 2014, the audience is introduced to Elixir, a functional and concurrent programming language built on the Erlang VM. The speaker encourages Ruby developers to explore Elixir to embrace concurrency without the complexities often associated with threading in Ruby.

Key Points Discussed:

  • Introduction to Elixir: Elixir was developed by José Valim and is designed to leverage Erlang’s capabilities in creating reliable, fault-tolerant distributed systems.
  • Concurrency: The speaker highlights the challenges in Ruby concerning concurrency and describes how Elixir offers a more manageable approach using the actor model, where processes communicate via messages without shared memory, reducing bugs.
  • Elixir Tooling: Key tools such as Interactive Elixir (IEX) for interactive coding and Mix for project management are discussed, showcasing Elixir's easy-to-use environment.
  • Unique Language Features: The audience learns about features like the Pipe operator, which simplifies data transformation, and the concept of immutability in data structures, ensuring the original data remains unchanged.
  • The Actor Model: Benjamin delves into how Elixir implements an actor system where processes can run independently, communicate asynchronously, and handle failures gracefully through the use of supervisors that restart child processes if they fail.
  • Building an HTTP Load Tester: The speaker demonstrates developing a simple HTTP load tester, Blitzy, showing sequential and concurrent processing and how it can run in a distributed fashion across multiple nodes in the network.
  • Community and Resources: The growing Elixir community and available resources such as the Phoenix framework and upcoming conferences are highlighted, encouraging attendees to get involved and learn.

Conclusion and Takeaways:

  • Benjamin emphasizes the importance of exploring functional programming in the context of increasing concurrency in software development. He inspires Rubyists to broaden their programming horizons and experience the benefits of Elixir, stating that it brings joy in programming through its unique constructs and fault-tolerant systems. This talk ultimately encourages developers to be proactive in adapting to modern programming paradigms as concurrency becomes increasingly unavoidable.
00:00:18.240 Hi! I'm super excited to be here. My name is Benjamin, and today I'm inviting everyone to have a sip of Elixir. I'm from Singapore, and I took three flights and 20 hours just to get here and join everyone over at RubyConf. Thank you!
00:00:41.840 I'll do the obligatory employer thing: my trip has been made possible by Neo, who has kindly sponsored my tickets and accommodation. Obviously, we are hiring; we have offices in multiple locations in the US, and we have one in Singapore. Please come speak to me if you're interested. Also, a huge thank you to the RubyConf committee for organizing this entire conference. It's amazing, and I'm grateful for the opportunity to speak to all of you.
00:01:19.920 I must say the RubyConf committee has a great taste in choosing my talk! I have a confession: I have never written a single line of code using threads in Ruby. Threads somehow feel like the wrong kind of abstraction to think about concurrency, involving synchronization, condition variables, critical sections, and the list goes on. There are so many different ways to shoot yourself in the foot.
00:01:41.840 I persevered. I love Ruby, but concurrency wasn't one of Ruby's strongest points, as you've seen in various other talks. Yet, I wanted to be able to program concurrently; I wanted to be able to program for multicore computers without losing my sanity.
00:02:15.640 In 2005, a famous article titled 'The Free Lunch is Over' made its rounds around the internet. It stated that the speed of serial microprocessors was already reaching its physical limit. This means that in order to increase performance, companies such as Intel and AMD would have to squeeze more cores into a single chip, instead of just increasing the raw clock speed. More importantly for us software developers, this means we would be forced to write massively multithreaded programs to make use of such processors.
00:02:44.560 So now, not only could I not write concurrent programs, but no one was going to give me any free lunch. Life was starting to look pretty bleak. I stumbled onto Elixir, mostly due to the hype. I've been playing with it for a little over a year now, which makes me an Elixir expert, but I am really having so much fun. I think it would be almost criminal not to share this with you.
00:03:24.720 So, of course, I'm not coming to RubyConf and asking you to switch languages. Instead, I'm encouraging each and every one of you to be more promiscuous in your languages. Elixir just happened to be the language choice I picked. Here's what we'll learn today: we'll look at Elixir tooling, and we will also explore the concurrency and fault-tolerance features in Elixir.
00:03:49.920 I have recorded a few examples since live coding is hard, so we'll see some Elixir code in action. Finally, we will build a very simple HTTP load tester where we can see concurrency and distribution come together. Lots of interesting things to learn and many fun things to do—let's do this.
00:04:22.199 Elixir was created by José Valim, who you might know from his work with libraries such as Devise and other Ruby frameworks. In a nutshell, Elixir is a functional and concurrent language built on the Erlang virtual machine. You might know that Erlang excels in building soft real-time concurrent distributed systems, and its reliability is pretty legendary.
00:04:46.880 I hope not many of you have used Erlang before; otherwise, you'll notice a lot of loopholes in the story I'm going to tell you next. Erlang was born in Ericsson's C.L. in Stockholm, Sweden. Joe Armstrong and three fine gentlemen created Erlang because Ericsson wanted to find a better way to program telephone switches.
00:05:14.280 Here's roughly how a telephone switch works: a caller makes a phone call, and the request arrives at the switch. The switch then connects to the receiver. Obviously, the switch has to be able to handle multiple calls coming in and out, but not only that, each telephone switch should be able to communicate with each other. In the 1980s, Ericsson had a programming language that could run on these telephone switches.
00:05:55.919 Unfortunately, there were two problems: the first was that it took too long to write programs in that language, and secondly, that language had a problem in that it couldn't turn these telephone switches into true multiprocessor systems. Erlang was therefore created to suit these needs and tackle this problem.
00:06:13.680 There were a few relatively small and obscure companies using Erlang; you might recognize some of them. So what does Elixir bring to the table? Is it just another JavaScript for Erlang? Thankfully, not. Elixir offers many features that improve upon Erlang, and using Elixir allows you to leverage Erlang libraries, and the reverse is also true.
00:06:54.680 Now comes the part where I try to convince you why Elixir is worth your time. Let's first talk about tooling.
00:07:03.080 The first tool I'm going to show you is Interactive Elixir, or IEX for short. In the Ruby world, it's like IRB. This is a Read-Eval-Print Loop (REPL) where you will spend most of your time. This demo will show you how Elixir looks and also demonstrates the built-in documentation system, which is written in Markdown.
00:07:25.440 The second tool I'm going to show you is Mix. Mix allows you to create an Elixir project quickly and has tasks to handle things like installing dependencies and running unit tests. Now, we are going to see some Elixir code.
00:07:53.720 Allow me to introduce what I consider to be one of the most awesome language features in Elixir: the Pipe operator. Here is some valid Elixir code. This code takes a range of 1 to 10, squares each element, and then selects the elements that are larger than 10. No big deal, but I think this code is terrible. In order to understand this code, you must locate the first argument in the innermost function and then work your way out. Using pipes, we can express the same computation in a much prettier way.
00:08:52.640 In fact, you can think of this computation as a series of data transformations. There is an even shorter way to express this computation using the function capture operator. The outermost ampersand (&) expands to the anonymous function, while the ampersand 1 represents the first argument in the function. Let's see a slightly more realistic example where I want to pass a list of places given this JSON.
00:09:50.560 Here's one way we can parse this JSON using pipes. When we execute the 'get_body' function, another copy of the JSON hash is returned with a result as its root node. Notice I said 'copy' because in Elixir, data structures are immutable. This means that when you modify a data structure, a modified copy is returned, leaving the original one untouched.
00:10:09.320 Similarly, 'get_result' returns places as its root node, and finally, we get a list of places. From there, it's just a matter of mapping through all the places and processing them accordingly.
00:10:34.560 Now let's take a little break, and I'll show you some interesting things about Singapore along the way. This is a typical road sign in Singapore. I've lived in the US for one year, and seriously, the US has really nice road signs!
00:10:45.080 We love our acronyms, and everything circled here is either a name of an expressway or a building in Singapore. We love our acronyms so much that we even have a Wikipedia entry on them. Before we go any further, let's talk about the actor concurrency model in Elixir.
00:11:05.080 In general, the actor concurrency model has the following properties: when we talk about actors, they are also called processes that perform a specific task and communicate using sending and receiving messages. Actors respond to really specific messages, and in Elixir, these messages are pattern matched. Finally, processes do not share memory, which eliminates an entire class of concurrency bugs.
00:12:06.639 Now let's go to the fun stuff: we will see how to write a concurrent program in Elixir. Processes are the basic concurrency primitive in Elixir, but do not confuse them with operating system processes; they are not the same. These processes are independently created and managed by the Erlang virtual machine. This is a process, and that is a process ID (PID). If I'm lazy, you'll hear me refer to the PID as 'pid' for short.
00:12:56.360 A process communicates by sending and receiving messages. If you know the PID of any process, you can send it a message. Messages are sent asynchronously, much in a 'fire-and-forget' fashion. This means that once a process sends a message, it returns immediately and continues to handle the next computation. Allow me to introduce you to the 'send' function. In my opinion, this is my favorite hello world example.
00:13:59.760 While using math functions as examples can be annoying, I have a perfectly legit reason, so bear with me. We'll see more real examples soon. Take a look at this function: it takes in two arguments, M and N, where both must be zero or more. The first two cases are pretty straightforward, but it's the third case that gets interesting. When both M and N are larger than zero, the second argument calls itself.
00:14:38.760 This means the function's value grows rapidly, and even for really small outputs, like 'eomen(43)', it results in a ridiculously huge number. This is the 'eomen' function in Elixir. We will now take a look at our 'eomen' function again, but this time we'll see how we can use processes. The reason we want to do this is to enable concurrency; that is, we can fire multiple computations at the same time.
00:15:24.920 The top part of the function you've seen before; that's the usual 'eomen' function. The loop function is new. When the loop function is executed in a process, the process can send and receive messages. Let's see how we can do that! To create a process, we use the 'spawn' function, which takes in three arguments: the module name, the function name, and the arguments for the function.
00:16:04.619 The return value of the spawn function is a PID. Now our process is ready to receive and send messages. Let's send the process W1 a message. The built-in 'send' function takes in the process ID and the message we want to send. The sent message is then pattern matched in the receive block. When the pattern matches, the body is executed. Finally, the loop recursively calls itself so that it can handle the next message.
00:16:42.600 Without the loop, the process dies and gets garbage collected by the Erlang virtual machine. When the process receives anything other than a two-element tuple, the second catch-all pattern is matched. Similar to the previous example, the body is matched, and the result is printed.
00:17:21.359 I want to show you that we can run functions directly. That is, we are not running the functions in a process. For simple computations, that's probably fine, but once you hit more computationally intensive operations, you will block the shell.
00:17:56.920 In this case, when we give it a very hard job to handle, the IEX session will be blocked until the computation is done. Things get a little more fun now as we create a process response. We'll send the process the same message as before, but since we are running in a process, this will not block the IEX session.
00:18:32.640 Again, notice that for simple jobs, the result returns almost immediately, but for more complex jobs, the message is returned, but the caller is not blocked. We will have to wait a while longer for the result to appear, but unlike in the previous case, the shell session is not blocked in any way.
00:19:05.000 Let's take things up a notch. We are going to start four processes and give each of them a computation-intensive job to handle. My computer has four cores in it. Wait for it, and look at it—all four cores are lit up completely. I always get a warm fuzzy feeling when I put my hardware to good work.
00:19:44.080 So what happens when we send a message to a busy process? All that happens is the messages get buffered. Alright, something interesting about Singapore: guess what all these people are queuing up for! In Singapore, we love to queue for things: coffee, bubble tea, freebies.
00:20:18.440 Let's talk about one of the coolest things that Elixir inherits from the Erlang virtual machine—fault tolerance, which is the system's ability to stay up in the presence of failure. Supervisors are an example of how the virtual machine implements fault tolerance. The green ball is the supervisor; it is just a process. Its only job is to monitor child processes.
00:20:55.200 But supervisors can also supervise other supervisors, meaning we can create supervision trees that are layered to form an even bigger supervision tree. When a child process dies, the supervisor can react in several ways. For example, it can simply restart the failed process.
00:21:16.240 Or! Here's another way the supervisor can react: if the same child process dies, the supervisor can also terminate all the child processes under the tree and then restart them in the order they were created. I'm now going to show you a demo of how a supervisor restarts a child process.
00:21:38.760 These are the processes started up when you launch an IEX session. The green and blue balls represent processes. The green stuff you can basically ignore. So I'm going to create 50 worker processes in one of the supervisors. Watch what happens when I kill everything. The supervisor automatically starts everything up again! Now I’m going to create more workers.
00:22:20.760 Check that out! Isn’t that awesome? Erlang and Elixir programmers have adopted the 'let it crash' motto. In general, we do not spend so much time on defensive programming because someone is bound to trip over the wire; your bugs are going to happen. Instead, they built supervisor hierarchies to ensure that when something breaks or blows up, another process can take over and restore the system into the next good state automatically.
00:22:52.600 We've come to the last part, and I think the coolest part of my talk. Let's build a very simple HTTP load tester. We'll implement our load tester in a series of iterations, and the first one will obviously be the simplest. This load tester is implemented as a command line program I’ve called 'Blitzy.' The program takes in a '-n' flag followed by a number which states the total number of workers to run. This is followed by the URL of the site we want to test.
00:23:17.720 Let’s first understand the interaction between the processes. When we launch the program, a coordinator process starts up, and a bunch of workers are created. First, we tell the coordinator process how many workers it has to handle. The coordinator also keeps track of the number of workers it has heard from, initially set to zero. When a worker completes a successful GET request, it sends a success message to the coordinator process.
00:23:58.760 After receiving a message from the worker, the coordinator process increments the count of workers from whom it has heard by one. If another worker sends over a failure message, the same thing happens: the coordinator increases the count and waits for the last worker.
00:24:30.320 In the simplest version, we will not use any concurrency here. The worker process is not complicated; its only job is to make a GET request, time it, and then send the results back to the coordinator process. The worker can receive at least two different responses from the HTTP client. In this case, we only care when an HTTP client returns a successful response or when it times out.
00:25:08.040 If the GET request is successful, the first 'do_request' function clause will be matched, and the time elapsed is returned. Otherwise, it will return a tuple with an error as its first element: 'error unknown tuple' is returned for any other value from the HTTP client.
00:25:43.680 One of these values will eventually be sent to the coordinator process. Let's examine the coordinator process code. We first give the coordinator process a name; in this case, it has the same name as the module name. When a process is registered, it can be accessed using its name instead of its PID. The main work is done in the 'do_process_workers' function.
00:26:12.480 We define two versions of 'do_process_workers' to handle two specific cases. The first one is the base case when the number of processed workers equals the total number of workers we have heard from. This means that the coordinator process knows it can return the results. Otherwise, the coordinator must wait for a worker to send it another successful or failure message.
00:26:52.600 The entry point of our command line program is the main function. The 'do_request' function is where we start the coordinator and worker processes. Now, let's take a slight detour and talk about tasks. A task is a bit like a future; if you're familiar with them. Let me explain with an analogy.
00:27:25.200 When you create a task, you not only create a process; it comes with a container. You specify the module, the function, and it does the computation in a contained environment while running in the background. When you want to find out what value is inside, you call 'task.await' to check if the value is available. If the task is busy, you'll have to block.
00:27:55.040 So a task allows you to do some work in a separate process and lets you collect results as needed. Running a task is very simple; you call 'task.async' and pass the necessary arguments. When you're ready to retrieve the result, you simply call 'task.await.' Let's see an example.
00:28:28.360 So again, we're running the 'ean' example here. For simple computations, it returns immediately; for more complex ones, we have to wait a bit. Let's go back to our HTTP load tester: we start the coordinator process in a task.
00:28:50.680 A coordinator process is initiated in a separate process and then waits for workers to communicate with it. Each worker in this case starts one by one. The problem here is that each worker must complete the GET request before the next worker's execution. So, here we do not have concurrency.
00:29:38.240 Finally, 'task.await' is called to retrieve the return value of the coordinator process—that is, the accumulated results from the messages received by the workers. When we run this, notice that the workers are returned in order of creation.
00:30:07.800 Now, let's make our load tester concurrent. How much code do you think we need to add? In this case, the only thing we change is in the workers' loop: we use the 'spawn' function to create and launch a separate process for each worker. That's it! The workers now run concurrently.
00:30:50.560 Let's see this in action. There you have it! The workers come back in no specific order. Now, let's enhance our load tester to be distributed.
00:31:05.440 Here we have two nodes connected to the same network. Processes in Erlang and in a cluster are location transparent, meaning that message passing between two processes on the same node is just as easy as sending a message between two different nodes, as long as you know the process ID.
00:31:45.680 To run our load tester distributedly, we need to do a bit of extra work. First, we set the current node as the master node. This line connects to all the slave nodes. We then pass a list of connected nodes to the process function and call 'do_request', which you see here.
00:32:21.160 First, we calculate how many workers we should run on each node. So for a four-node cluster with an N value of 1000, we are going to spawn 250 workers on each node. Here we are starting up the coordinator task remotely on each of the four nodes.
00:32:53.680 What is returned is a list of tasks: three of which come from the remote nodes, and one from the main one. Next, we're going to start 250 workers on each of the four nodes. All we need to do is iterate through each node, spawning the workers remotely using the 'node.spawn' function with the necessary arguments.
00:33:26.760 Finally, just as before, we call 'task.await' to collect our results back from the local and remote nodes and local and remote coordinators once the worker processes have completed their jobs and exited.
00:33:46.520 This is all the code you need to set up and run the HTTP load tester distributedly. Let's see an example of this in action. We will first start up three slave nodes, and then we will run the main program. And we are done! We are running distributedly.
00:34:18.800 In Singapore, this is a common scene in food courts. This is how we reserve seats: we use anything from tissue packets or bags to cell phones, and if you happen to be a doctor, we also use stethoscopes.
00:34:50.480 The Elixir community is growing nicely. One of the most popular projects in Elixir is the Phoenix framework created by Chris McCord. One of its really nice features is that it makes working with web sockets incredibly simple.
00:35:31.440 Josh Adams has also created Elixir Sips, modeled something like a Grim's Ruby Tapas. It's totally worth checking out! There’s an Elixir conference that was held this year organized by Jim Freeze, who is in the audience, and there’s another one next year, this time in Poland.
00:36:07.120 In my copious free time, I blog about Elixir stuff mostly, and if you want to learn more, there are already some books available. More importantly, I’m also writing a book and currently waiting for my publisher to get it into the hands of readers.
00:36:43.160 So, I need to thank two ladies: one is Hal Miller, whose slide designs I shamelessly copied, and the other is my wife, who allowed me to ignore her during the preparation of this presentation. I also want to thank my teammate at Neo, who has been extremely supportive of me going to conferences to speak at rather than doing client work.
00:37:17.160 If you haven't started worrying about concurrency yet, now is the perfect time to be paranoid. The world is concurrent, but how we program isn't. I also think this is the best time to get into functional programming. Dave Thomas said it best at the recent GoTo conference in Chicago.
00:37:53.640 He said, and I quote, 'There is a word for people not doing functional programming five years from now. That word is maintenance programmer.' Elixir has opened up many opportunities for learning.
00:38:22.240 Personally, I'm having so much fun learning about functional, concurrent, and distributed programming, and I encourage you to go out and try different languages. Find your own Elixir! It is truly a wonderful time to be a developer.
00:38:41.960 Now thank you all for being a wonderful audience. I would love to answer any questions you may have. I'm a very shy person, so if you come up to me after my talk, I would be more than happy to speak to all of you. Thank you very much.