Part 4. Making operations on the key-value store faster by using worker processes — Elixir/OTP

Arpit Dubey
Gamezop Tech
Published in
4 min readFeb 26, 2020

This is part 4 of the 7 part series. Please read the previous articles to catch up on the topic.

The save request may not seem problematic from the client-side perspective, because it’s an asynchronous cast. A client issues a store request and then goes about its business. But if requests to the database come in faster than they can be handled, the process mailbox will grow and increasingly consume memory. Ultimately, the entire system may experience significant problems, resulting in the possible termination of the BEAM OS process.

The get request can cause additional problems. It’s a synchronous call, so the key-value server waits while the database returns the response. While it’s waiting for the response, this to-do server can’t handle new messages. What’s worse, because this is happening from initialization, the manager process is blocked until the list data is retrieved, which ultimately may render the entire system useless under a heavier load.

It’s worth repeating that the synchronous call won’t block indefinitely. Recall that GenServer.call has a default timeout of five seconds, and you can configure it to be less for better responsiveness. Still, when a request times out, it isn’t removed from the receiver’s mailbox. A request is a message that’s placed in the receiver’s mailbox. A timeout means you give up waiting on the response, but the message remains in the receiver’s mailbox and will be processed at some point.

Let’s get started 🏎️

Before beginning make sure you have Elixir and Mix installed in your system.

Now create a mix project by typing

mix new ex4_key_val_with_db_workers

This command should create your project structure like this

project structure

Delete all the files in the lib folder and create a file with name db_worker.ex. Also, copy the other files from the 3rd exercise into the lib folder i.e. manager.ex, server.ex, database.ex and store.ex.

As discussed we are facing a bottleneck when a call is made to the database to read the data if the number of requests is increased tremendously because the request is a synchronous call.

In order to address this issue, we can use something called pooling. What pooling essentially means that a load of reading and writing to the database would be offloaded to certain worker processes by the database so that if a worker is busy fetching the value from the file the other worker can instantaneously pick up the new request.

So, let’s look at how we will achieve this implementation. Instead of letting the database GenServer do the heavy lifting of saving and retrieving the data in the file we would direct the task to another GenServer process which we would call a db_worker. The only thing that database process would do is that it will create and store a pool of db_worker in its internal state and randomly or in a round-robin fashion would select a worker and would assign the task to it.

High-Level Design

Show me some code 👨‍💻

Here we now have 5 entities to care about.

  1. Database Worker
  2. Database
  3. Manager
  4. Server
  5. Store

As you can see the Database process when started with the init/2 command spawns 5 worker which call a start/1 which starts another GenServer for the worker processes.

The save save/2 and fetch/1 and fetch functions now act like much of abstraction and sends all the task to any randomly chosen database worker.

The get_worker function randomly chooses any number from 1–5 and returns it on the basis of what we would select our worker process.

database.ex

The database worker on the start receives the path where the database folder is present. The worker now handles the retrieval and saving of store values into the files.

db_worker.ex

Rest all the files areas in the previous exercise so don’t mind to go back and have a look at those too.

The final run 📟

In order to start the whole system, we first initiate an elixir session
by typing

iex -S mix
session

Now as you can see that we start our database inside our Manager and wherever we start our database 5 different workers are instantly spawned with the database to handle the task.

Saving data into the database may be handled by a different worker and retrieving value might be handled by a different one. It is not a concern for us to decide which worker to select.

This approach of pooling will certainly remove the bottleneck to the database that we are facing.

So this is how we successfully added pooling strategy to the key-value store.

I hope this post has helped you get a little bit better understanding of the whole process.

The complete source code of all the parts are here.

--

--

Arpit Dubey
Gamezop Tech

Fullstack developer. React ● Node ● Go ● Elixir. I make awesome stuff with my bare hands 👐🏻