basho-labs/hierdis — pull request #6 by potatosalad (2013-08-29 03:09:37+00:00)
Title: Async interface and basic tests

As mentioned in #3, I threw together a port driver which makes use of the hiredis asynchronous API. Primarily because I wanted to make use of hiredis for some projects that use redis for pub/sub. Technically the NIF can be used as follows:

```erl
1> {ok, C} = hierdis:connect("127.0.0.1", 6379).
{ok,<<>>}
2> hierdis:command(C, ["SUBSCRIBE", "channel"]).
{ok,[<<"subscribe">>,<<"channel">>,1]}
3> hierdis:get_reply(C).
{ok,[<<"message">>,<<"channel">>,<<"test">>]}
```

With the caveat that `hierdis:get_reply/1` [blocks the entire thread](http://www.erlang.org/doc/man/erl_nif.html#lengthy_work).

The port driver has port-instance locking in comparison. Here's the same pub/sub example with the port driver.

```erl
1> {ok, C} = hierdis_async:connect("127.0.0.1", 6379).
{ok,#Port<0.741>}
2> hierdis_async:command(C, ["SUBSCRIBE", "channel"]).
{ok,[<<"subscribe">>,<<"channel">>,1]}
3> receive Reply -> Reply end.
{redis_message,#Port<0.741>,
[<<"message">>,<<"channel">>,<<"test">>]}
```

This is my first attempt at writing a port driver for Erlang, so hopefully the Erlang API and implementation are not too terrible.

Just like [`gen_tcp:controlling_process/2`](http://erlang.org/doc/man/gen_tcp.html#controlling_process-2) there is a `hierdis_async:controlling_process/2` to change the owner of the port. Only the owner of the port may execute `hierdis_async:command/2` or `hierdis_async:close/1` commands. Any process can execute `hierdis_async:append_command/2` however.

Here is the version of the README examples using `hierdis_async` instead of `hierdis`.

##### Issue single commands to Redis as an `iolist`.

```erl
$ erl -pa ebin -eval 'application:start(hierdis)'

Erlang R16B01 (erts-5.10.2) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

Eshell V5.10.2 (abort with ^G)
1> {ok, C} = hierdis_async:connect_unix("/tmp/redis.sock").
{ok,#Port<0.741>}
2> hierdis_async:command(C, ["SET", "foo", "bar"]).
{ok,<<"OK">>}
3> hierdis_async:command(C, ["GET", "foo"]).
{ok,<<"bar">>}
4> hierdis_async:command(C, ["MSET" | ["key1", "1", "key2", "2", "key3", "3"]]).
{ok,<<"OK">>}
5> hierdis_async:command(C, ["GET", "key1"]).
{ok,<<"1">>}
6> hierdis_async:command(C, ["GET", "key3"]).
{ok,<<"3">>}
7> hierdis_async:command(C, ["MGET" | ["key1", "key2", "key3"]]).
{ok,[<<"1">>,<<"2">>,<<"3">>]}
```

##### Pipeline commands to Redis as a `list` of `iolist`s.

Can be done manually using `hierdis_async:append_command/2`. Still needs to be implemented in `hierdis_async`, I'll probably submit a separate pull request.

##### Execute a transaction pipeline against Redis as a `list` of `iolist`s.

Same as above.

##### Manually append commands and get replies.

```erl
8> hierdis_async:append_command(C, ["MULTI"]).
{ok,15}
9> hierdis_async:append_command(C, ["SET", "foo", "pipelined"]).
{ok,37}
10> hierdis_async:append_command(C, ["SET", "bar", "linedpipe"]).
{ok,37}
11> hierdis_async:append_command(C, ["EXEC"]).
{ok,14}
12> flush().
Shell got {redis_reply,#Port<0.741>,<<"OK">>}
Shell got {redis_reply,#Port<0.741>,<<"QUEUED">>}
Shell got {redis_reply,#Port<0.741>,<<"QUEUED">>}
Shell got {redis_reply,#Port<0.741>,[<<"OK">>,<<"OK">>]}
ok
```

You can also use the port driver directly with `erlang:open_port/2` and `erlang:port_call/3`. There are 5 message types that are sent back:

```erlang
%% connect/2,3 or connect_unix/1,2 was successful
{redis_opened, Socket :: port()}.

%% result of close/1 or some other error rendering the port unusable
{redis_closed, Socket :: port()}.

%% when hiredis is in the "SUBSCRIBED" context
{redis_message, Socket :: port(), Message :: term()}.

%% when hiredis is in the normal context
{redis_reply, Socket :: port(), Reply :: term()}.

%% an error has occurred, a fatal error will be followed by a redis_closed
{redis_error, Socket :: port(), {Type :: atom(), Reason :: string()}}.
```

I also wrote some common test...tests as mentioned in #2. Well, technically you were talking about EUnit, so hopefully ct is also acceptable. It just does some basic tests on the synchronous as well as the asynchronous API.

After a couple of tweaks I got it working on travis: [![Build Status](https://travis-ci.org/potatosalad/hierdis.png?branch=async-tests)](https://travis-ci.org/potatosalad/hierdis)

I used [libumem](https://blogs.oracle.com/dlutz/entry/memory_leak_detection_with_libumem) on SmartOS and fixed a few obvious memory leaks as well, but there may be others present.

I ran a quick benchmark on a 2011 MacBook Pro with basho_bench and got roughly ~30k ops/sec throughput (~35k ops/sec using the synchronous API). I didn't save the test results, unfortunately.

Let me know if you have any problems with the port driver or if it's out of scope for what you're hoping to do with hierdis. Thanks!

[![Show Me Potato Salad](https://f.cloud.github.com/assets/1804/1047087/d6f2ee24-1056-11e3-94c4-9782ee5ecc45.jpg)](http://www.youtube.com/watch?v=prkJ_EshEWk)