Adding our First Commands

Now that we have everything set up and we know how commands are implemented it’s time to implement our own.

To start we are going to implement a simple in memory key value store, the first two commands we are going to implement are the basic ones we need to see if it works: put and get

To hold the values we are going to use the Erlang Term Storage (ETS).

Riak Core API

First we start by creating the two new metrics for our new commands.

Then we add the commands to tanodb.erl, get and put we extract the common code to hash the key to a vnode to a private function called send_to_one.

On the riak_core side, that is, in the tanodb_vnode module, on init we create our ETS table, the name is tanodb_<partition> where <partition> is the partition id.

Then we add two new function clauses to handle_command, one for put and one for get. The logic is quite simple.

The code from tanodb.erl:

get(Key) ->
    tanodb_metrics:core_get(),
    send_to_one(Key, {get, Key}).

delete(Key) ->
    tanodb_metrics:core_delete(),
    send_to_one(Key, {delete, Key}).

put(Key, Value) ->
    tanodb_metrics:core_put(),
    send_to_one(Key, {put, Key, Value}).

% private functions

send_to_one(Key, Cmd) ->
    DocIdx = riak_core_util:chash_key(Key),
    PrefList = riak_core_apl:get_primary_apl(DocIdx, 1, tanodb),
    [{IndexNode, _Type}] = PrefList,
    riak_core_vnode_master:sync_spawn_command(IndexNode, Cmd,
         tanodb_vnode_master).

The relevant code from tanodb_vnode.erl:

handle_command({put, Key, Value}, _Sender,
               State=#state{table_name=TableName, partition=Partition}) ->
    ets:insert(TableName, {Key, Value}),
    {reply, {ok, Partition}, State};

handle_command({get, Key}, _Sender,
               State=#state{table_name=TableName, partition=Partition}) ->
    case ets:lookup(TableName, Key) of
        [] ->
            {reply, {not_found, Partition, Key}, State};
        [Value] ->
            {reply, {found, Partition, {Key, Value}}, State}
    end;

handle_command({delete, Key}, _Sender,
               State=#state{table_name=TableName, partition=Partition}) ->
    case ets:lookup(TableName, Key) of
        [] ->
            {reply, {not_found, Partition, Key}, State};
        [Value] ->
            true = ets:delete(TableName, Key),
            {reply, {found, Partition, {Key, Value}}, State}
    end;

Test it

Stop, build, start and in the console we run some commands.

First try getting the key “k1” from bucket “mybucket”, which doesn’t exist:

(tanodb@127.0.0.1)2> tanodb:get({<<"mybucket">>, <<"k1">>}).

{not_found,228359630832953580969325755111919221821239459840,
           {<<"mybucket">>,<<"k1">>}}

We get not_found back with the partition that handled the command and the bucket and key that wasn’t found.

Now let’s put that key:

(tanodb@127.0.0.1)3> tanodb:put({<<"mybucket">>, <<"k1">>}, 42).

{ok,228359630832953580969325755111919221821239459840}

We just get ok back, let’s try to get it again:

(tanodb@127.0.0.1)4> tanodb:get({<<"mybucket">>, <<"k1">>}).

{found,228359630832953580969325755111919221821239459840,
       {{<<"mybucket">>,<<"k1">>},{{<<"mybucket">>,<<"k1">>},42}}}

Now we get the value back.

Let’s try the same with another key:

(tanodb@127.0.0.1)5> tanodb:get({<<"mybucket">>, <<"k2">>}).

{not_found,1210306043414653979137426502093171875652569137152,
           {<<"mybucket">>,<<"k2">>}}

Notice that the partition id changed, this is because the key hashed to a different vnode.

(tanodb@127.0.0.1)6> tanodb:put({<<"mybucket">>, <<"k2">>}, 42).

{ok,1210306043414653979137426502093171875652569137152}
(tanodb@127.0.0.1)7> tanodb:get({<<"mybucket">>, <<"k2">>}).

{found,1210306043414653979137426502093171875652569137152,
       {{<<"mybucket">>,<<"k2">>},{{<<"mybucket">>,<<"k2">>},42}}}

REST API

Let’s expose our new functions as a REST API, first we add a new route to cowboy for our store, the API will be like this:

  • POST /store/:bucket/:key <json-body>: stores <json-body> under {:bucket, :key}
    • returns 204 No Content on success
  • GET /store/:bucket/:key:
    • returns 404 if :key doesn’t exist on :bucket
    • returns 200 and the value stored under {:bucket, :key} if found

The implementation of the store api is quite simple if you know cowboy, it’s in the tanodb_http_store.erl file.

Test it

Do the usual stop, build, run and then from another shell:

$ http localhost:8080/store/mybucket/bob

Returns

HTTP/1.1 404 Not Found
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:16:16 GMT
server: Cowboy

Let’s put something on that bucket/key:

$ http post localhost:8080/store/mybucket/bob name=bob color=yellow
HTTP/1.1 204 No Content
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:17:25 GMT
server: Cowboy

And try to get it again:

$ http localhost:8080/store/mybucket/bob
HTTP/1.1 200 OK
content-length: 31
content-type: application/json
date: Fri, 30 Oct 2015 17:18:06 GMT
server: Cowboy

{
    "color": "yellow",
    "name": "bob"
}

Implementing Delete

Let’s implement the delete command and REST API so our API is complete.

We start as usual adding the metrics for the delete command, then add the delete function on the tanodb module which is really similar to get.

After that we add the new function clause in handle_command in our vnode, notice that it returns the same values as get, this is to get back the last value in case it was found or inform us that there wasn’t a value with that bucket and key.

Finally we handle the DELETE HTTP method in our cowboy handler.

Test it

Let’s start by testing the core API, we get a key that is not there:

(tanodb@127.0.0.1)1> tanodb:get({<<"mybucket">>, <<"k1">>}).

{not_found,228359630832953580969325755111919221821239459840,
           {<<"mybucket">>,<<"k1">>}}

Then set it to the value 42:

(tanodb@127.0.0.1)2> tanodb:put({<<"mybucket">>, <<"k1">>}, 42).

{ok,228359630832953580969325755111919221821239459840}

Get it to make sure it’s there:

(tanodb@127.0.0.1)3> tanodb:get({<<"mybucket">>, <<"k1">>}).

{found,228359630832953580969325755111919221821239459840,
       {{<<"mybucket">>,<<"k1">>},{{<<"mybucket">>,<<"k1">>},42}}}

Proceed to delete it, notice that it returns the last seen value and the result has the same shape as a get call:

(tanodb@127.0.0.1)4> tanodb:delete({<<"mybucket">>, <<"k1">>}).

{found,228359630832953580969325755111919221821239459840,
       {{<<"mybucket">>,<<"k1">>},{{<<"mybucket">>,<<"k1">>},42}}}

We get it again to make sure it was deleted:

(tanodb@127.0.0.1)5> tanodb:get({<<"mybucket">>, <<"k1">>}).

{not_found,228359630832953580969325755111919221821239459840,
           {<<"mybucket">>,<<"k1">>}}

And try to delete it again to see how it handles trying to delete a key that is not there:

(tanodb@127.0.0.1)6> tanodb:delete({<<"mybucket">>, <<"k1">>}).

{not_found,228359630832953580969325755111919221821239459840,
           {<<"mybucket">>,<<"k1">>}}

Now that we checked it works on the Erlang shell, let’s try the REST API, we will do the same as before, first get and expect not found:

$ http localhost:8080/store/mybucket/bob

HTTP/1.1 404 Not Found
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:32:17 GMT
server: Cowboy

Then POST a value:

$ http post localhost:8080/store/mybucket/bob name=bob color=yellow
HTTP/1.1 204 No Content
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:32:21 GMT
server: Cowboy

GET it to make sure it’s there:

$ http localhost:8080/store/mybucket/bob
HTTP/1.1 200 OK
content-length: 31
content-type: application/json
date: Fri, 30 Oct 2015 17:32:23 GMT
server: Cowboy

{
    "color": "yellow",
    "name": "bob"
}

DELETE it:

$ http delete localhost:8080/store/mybucket/bob
HTTP/1.1 204 No Content
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:32:27 GMT
server: Cowboy

GET it back to make sure it’s actually deleted:

$ http localhost:8080/store/mybucket/bob
HTTP/1.1 404 Not Found
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:32:28 GMT
server: Cowboy

DELETE it again to see how it handles a missing delete:

$ http delete localhost:8080/store/mybucket/bob
HTTP/1.1 404 Not Found
content-length: 0
content-type: application/json
date: Fri, 30 Oct 2015 17:43:03 GMT
server: Cowboy