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_ where 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: .. code-block:: erlang 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: .. code-block:: erlang 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: .. code-block:: erlang (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: .. code-block:: erlang (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: .. code-block:: erlang (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: .. code-block:: erlang (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. .. code-block:: erlang (tanodb@127.0.0.1)6> tanodb:put({<<"mybucket">>, <<"k2">>}, 42). {ok,1210306043414653979137426502093171875652569137152} .. code-block:: erlang (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 : stores 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: .. code-block:: sh $ http localhost:8080/store/mybucket/bob Returns .. code-block:: http 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: .. code-block:: sh $ http post localhost:8080/store/mybucket/bob name=bob color=yellow .. code-block:: http 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: .. code-block:: sh $ http localhost:8080/store/mybucket/bob .. code-block:: http 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: .. code-block:: erlang (tanodb@127.0.0.1)1> tanodb:get({<<"mybucket">>, <<"k1">>}). {not_found,228359630832953580969325755111919221821239459840, {<<"mybucket">>,<<"k1">>}} Then set it to the value 42: .. code-block:: erlang (tanodb@127.0.0.1)2> tanodb:put({<<"mybucket">>, <<"k1">>}, 42). {ok,228359630832953580969325755111919221821239459840} Get it to make sure it's there: .. code-block:: erlang (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: .. code-block:: erlang (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: .. code-block:: erlang (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: .. code-block:: erlang (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: .. code-block:: sh $ 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: .. code-block:: sh $ http post localhost:8080/store/mybucket/bob name=bob color=yellow .. code-block:: http 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: .. code-block:: sh $ http localhost:8080/store/mybucket/bob .. code-block:: http 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: .. code-block:: sh $ http delete localhost:8080/store/mybucket/bob .. code-block:: http 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: .. code-block:: sh $ http localhost:8080/store/mybucket/bob .. code-block:: http 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: .. code-block:: sh $ http delete localhost:8080/store/mybucket/bob .. code-block:: http HTTP/1.1 404 Not Found content-length: 0 content-type: application/json date: Fri, 30 Oct 2015 17:43:03 GMT server: Cowboy