First blog post from my new position at Basho Technologies, home of Riak, a fast, flexible, scalable, operationally-easy data store written in Erlang. In this post, I examine gproc, an improved global process registry written in 2007 by Ulf Wiger, for possible use by Riak and Nitrogen.
Very often in Erlang, you want to create a process and give it a name. This allows you to send messages to that process from other processes without passing the Pid around. Erlang supports named processes using erlang:register(Name, Pid), but there are limitations.
These limitations constrain the way Erlang developers architect and debug applications. With a more robust process registry capable of storing additional metadata about each registered process, a developer can better decouple the responsibilities of an application, or gather information about running processes for debugging or reporting purposes.
Some concrete examples:
Riak spins off a new process for each get, put, and delete operation. There is currently no way to tell how many data operations are running at any one time, or to look inside a process once it is running to see what bucket it will modify. Ideally, this could be attached as metadata to the process, and a separate monitoring application could fetch this data and display it on the Riak Web dashboard.
Nitrogen spins off a process for a user’s session. Ideally, on subsequent requests Nitrogen could look up the session process in the registry based on the user’s cookie. For now, a separate lookup structure is used.
Ulf Wiger, back in his days at Ericcson (he is currently the CTO of Erlang Training and Consulting, noticed that he and other engineers were repeatedly (re)creating their own process registry structures in the form of lists and lookup tables as a way of working around the limitations described above.
To solve the problem in a more general fashion, Ulf created gproc. (Well, he first created proc, and then–with the insight he gained–created gproc.)
gproc lets you:
gproc can be confusing at first because naming processes, applying properties, and creating counters are all accomplished using gproc:reg(GProcKey, Value). This was done in the name of speed, consistency, code-reuse – all of these operations store data in the same ETS table, and so gproc makes functions double up.
Furthermore, to ensure thread safety, a process can only set it’s own values. To put it another way, it’s as if erlang:register(Name, Pid) became erlang:register(Name) and can only act on the current process.
Finally, it helps to think of “registering” a process as instead “the process sets a globally unique key”. Similarly, “setting a property” means “the process sets a non-unique key”.
You’ll see what I mean in the examples below.
To get a gproc playground, run the following:
svn checkout http://svn.ulf.wiger.net/gproc/branches/experimental-0906/gproc
cd gproc
erlc -I include -o ebin src/*.erl
erl -pa ebin
> application:start(sasl).
> application:start(gproc).
So let’s play. To store a value in gproc, call “gproc:reg({n, l, Key}, Value)”
Again, both Key and Value can be any Erlang term. With a type of n (for name) you can only associate one value with a key. Try it again, and gproc will throw an exception.
To remove something from gproc, call “gproc:unreg({n, l, Key})”. Alternatively, kill the process. gproc monitors all processes that have stored values. When a process dies, gproc automatically removes any values associated with that process. This can lead to some confusion when testing gproc in the Erlang shell. If you store some values in gproc, and then run a gproc operation that throws an exception, Erlang restarts your shell process, so you lose any gproc values associated with that process.
Though this can be confusing in the shell, this is exactly the behaviour we want from a process registry.
To get your stored value, call “gproc:get_value({n, l, Key})”. Note that this will only retrieve values stored by the current process. You can also use “gproc:get_value({n, l, Key}, Pid)” to get values from another pid.
In addition to your value, gproc stores the Pid responsible for the value. You can retrieve this by calling “gproc:lookup_pid({n, l, Key})” or “gproc:lookup_pids({p, l, Key})”. (You use the former when working with registered properties and aggregated counters, and the latter when working with properties and local counters.)
So you are probably wondering about the significance of the 3-tuple that we are passing to these functions. This tuple, of the form {Type, Scope, Key} tells gproc a few things about your value:
So far, we have been using a type of ‘n’, a globally unique key, which gives us a good mechanism for uniquely naming a process. This is similar to erlang:register/2.
Specifying ‘p’, for property, lets us set a key/value pair for a process, but other processes can also set a name and value of their own.
Specifying ‘c’, for counter, lets you set and increment a local counter for a process.
And specifying ‘a’ for aggregated counter, lets you access the sum of all local counters by the same name.
Note: we’re always using ‘l’ for local scope. See the ‘G is for Global’ note below on why I haven’t written anything about global scope.
To register a process in gproc, call “gproc:reg({n, l, Key}, ignored)”. The ‘ignored’ atom is irrelevant here, but could be used to store some extra metadata about the process.
Now you can use:
These functions are also available for registered processes, but less useful:
To set a property, call “gproc:reg({p, l, Key}, Value)”.
Now, you can use:
To create a local counter, call “gproc:reg({c, l, Key}, Integer)”.
Now, you can use:
To create an aggregated counter, call “gproc:reg({a, l, Key}, Ignored)”
Now you can use:
As you can see, gproc is really a shared dictionary with some limitations on who can store what when. Again, this leads to some confusion at first, but means that gproc can be applied to a wider variety of problems.
So far, we’ve glossed over gproc:select/1, and for good reason: it’s complicated. But, it’s also where gproc derives most of its power, so it pays to understand it.
With gproc:select/1, you can query the gproc process registry using roughly the same semantics as an ETS MatchSpec. Here is a quick (and far from complete) tutorial:
The basic form is gproc:select(Scope, MatchSpec).
Scope can either be ‘all’, ‘names’, ‘props’, ‘counters’, or ‘aggr_counters’. MatchSpec is of the form [{MatchHead, Guard, Result}]. (Yes, it must be wrapped within a list.)
The MatchHead is a 3-tuple of the form {GProcKey, Pid, Value}. GProcKey is the 3-tuple key we’ve seen earlier, ie: “{n, l, Key}”. By leaving an empty Guard ([]) for now, and using a catch-all Result ([’$$’]), we can already do some interesting things.
This will pull out every record in the process registry:
MatchHead = '_',
Guard = [],
Result = ['$$'],
gproc:select([{MatchHead, Guard, Result}]).
This will pull out every record where value == value1:
MatchHead = {'_', '_', value1},
Guard = [],
Result = ['$$'],
gproc:select([{MatchHead, Guard, Result}]).
You can specify a more complicated keys. This example will pull out any entry whose key would match “{myrecord, _}”
Key = {myrecord, '_'},
GProcKey = {'_', '_', Key}
MatchHead = {GProcKey, '_', '_'},
Guard = [],
Result = ['$$'],
gproc:select([{MatchHead, Guard, Result}]).
MatchHead can also use ‘$1’ variables, which lets you start using guards to shape your select. For example, this will pull out any gproc entry whose key is a list:
GProcKey = {'_', '_', '$1'},
MatchHead = {GProcKey, '_', '_'},
Guard = [{is_list, '$1'}],
Result = ['$$'],
gproc:select([{MatchHead, Guard, Result}]).
See this gist for more examples. http://gist.github.com/188032.
Normally, you would be able to use the Result property of the MatchSpec to select out a subset of the data you want to capture. In gproc this is slightly broken. You can choose which result you want to pull out by setting result to a ‘$1’ variable (for example, [‘$1’]), but currently you can only pull out one result from each match. In other words, [‘$1’, ‘$2’] does not work. (It only returns ‘$2’.)
It appears that GProc began to have support for QLC, which is pretty friggin’ sweet, but unfortunately I could not seem to make it work.
GProc automatically detects whether gen_leader is available on the system. If it is, then gproc will run in “global mode” meaning that all of the functions above work across your entire Erlang cluster rather than on a single node, which is also pretty friggin’ sweet. Unfortunately, gproc currently uses an obsolete version of gen_leader, so running gproc in global mode is not recommended.
GProc is extremely powerful, but the interface could use simplification. Convenience methods around gproc:reg/2 specifically shaped for registering processes would go far to help smooth the learning curve for newcomers.
In addition, gproc:select/1 has a few small bugs. The MatchSpec must be passed as a list, even though it will only work with one MatchSpec, and the Result portion of the MatchSpec can only be used with one ‘$N’ variable.
Gproc has now been moved to Github: http://github.com/uwiger/gproc.
I have added some functions, such as gproc:await(UniqueName, Timeout), which will suspend until the name key UniqueName becomes available, and then return a {Pid, Value} tuple representing the registration info. One way to use this is as a simple resource broker, or to manage dependencies during system start.
I have done some initial testing of the global parts of gproc. They seem to work. I used the hanssv+serge_version at http://github.com/uwiger/gen_leader_revival in which I have made small changes. The other gen_leader versions should work as well, but I haven’t tested them. See gproc_dist:elected/[2,3] to understand the differences (the hanssv+serge_version calls elected/3, while the others call elected/2).
Content © 2006-2021 Rusty Klophaus