Dev Corner

Tuesday, February 21, 2006

ETS is handy

The other day I wanted to create a counter that is shared amongst many clients that were connecting to a server. I wanted to assign a unique id to each, so I quickly listed how I would do this in erlang.

These are:
  1. Create a gen_server process that has private state that contains the counter variable
  2. Use ETS
  3. Make the accepting gen_server keep track of and assign incremental ids.
  4. ...
There are probably more, but those were a few that I quickly thought of.

So I started thinking about the create a new gen_server option. Perhaps it would have been a little overkill, and then I had to worry about what would happen, if by some odd chance, that the gen_server died.
If it did, then I would have to remember the state of the counter. It is true that the gen_server would be very simple that the chance of this happening are very slim, but still, you never know.

I thought about putting the counter somewhere and having the socket acceptor gen_server I had, assign the ids. I held off on that one, I didn't want to pollute the duties of that gen_server with lots of ancillary things.

So then I started looking into ETS. ETS stands for Erlang Term Storage. From the manual:
"... provide the ability to store very large quantities of data in an Erlang runtime system, and to have constant access time to the data."
I had used ETS before for storing server performance tables. I noticed that there was a function named: update_counter. This function updates a counter field in an ets table.
So I thought, cool, let me try creating some quick code to prototype this:

ETS stores tuples of information, so lets create a record that will be the data that is stored:

-record(counter_entry, {id, nextid=1}).

I added id in there to that I would be able to create several counters. Each counter sequence would be identifiable so that we can have many types of counters.

Next I added a function to initialize the counter table:

init(CounterID) ->
    ets:new(t_mycounters, [set, {keypos, 2}, public, named_table]),
    ets:insert(t_mycounters, #counter_entry{id=CounterID, nextid=1}).

When you create a new ets table, you can provide some options for how it is accessed and indexed. I passed in {keypos, 2}. This tells ets which tuple element number will serve as the index field of the table.
I gave the table public access, meaning that any other process can query and manipulate the table. Otherwise, only the process creator can manipulate the table.

The table is also a named_table. This way, I can use the table by passing in the atom: t_mycounters to identify the table. If I hadn't done this, then I should have stashed away the table identifier returned by ets:new and used that each time I wanted to operate on the table.

I then defined a function to get the next successive value for the counter:

getnext
(CounterID) ->
    ets:update_counter(t_freechatcounter, CounterID, {3, 1}).

ets provides a useful function to atomically treat a table field as a counter. In this case I told ets to update tuple position 3 by one. This is where the {3,1} comes in. It will actually be position 3 of the counter_entry record. Which is nextid.

Here is how to use it:

counters:init(chat_id_sequence).
NextSeqNo = counters:getnext(chat_id_sequence).

Kinda like the concept of postgresql sequences!

Well, that's it for now. If you were to take one thing away, it should be: ETS is handy and cool.