Riak Core Tutorial Part 9: Persistent KV with leveled backend

The content of this chapter is in the 07-leveled-kv branch.

https://gitlab.com/marianoguerra/tanodb/tree/07-leveled-kv

Implementing it

Until now we have an in memory key-value store, what do we have to do to make it a persistent one?

We would need to implement a new kv backend, that implements the same API as tanodb_kv_ets but using a library that persists to disk.

For this we are going to use leveled a pure erlang implementation of leveldb.

Being pure erlang means it's easy to build on any platform and easy to understand and contribute since it's all erlang!

The changes will involve making room for configurable KV backends, for that we will keep the backend module in a field called kv_mod in the vnode state:

-record(state, {partition, kv_state, kv_mod}).

On init we will pass an extra field to the KV backend init function with the base path where it can safely store files without clashing with other vnodes in the same node:

init([Partition]) ->
        DataPath = application:get_env(tanodb, data_path, "."),
        KvMod = tanodb_kv_leveled,
        {ok, KvState} = KvMod:new(#{partition => Partition,
                                                                data_path => DataPath}),
        {ok, #state { partition=Partition, kv_state=KvState, kv_mod=KvMod }}.

We are getting the base path to store data from an environment variable (tanodb.data_path), to make it configurable we need to add it to our cuttlefish schema on priv/01-tanodb.schema:

%% @doc base folder where data is stored
{mapping, "paths.data", "tanodb.data_path", [
  {datatype, directory},
  {default, "{{platform_data_dir}}/vnodes"}
]}.

Then we need to replace all the places in tanodb_vnode where we used tanodb_kv_ets to use the value of kv_mod from the state record.

On rebar.config we need to add the leveled dependency, since it doesn't have any release and it's not on hex.pm we will reference the master branch from the github repo:

{deps, [cowboy, jsx, recon,
        {riak_core, {pkg, riak_core_ng}},
        {leveled, {git, "https://github.com/martinsumner/leveled.git", {branch, "master"}}}
]}.

We specify in the release to load leveled and its dependency lz4:

{relx, [{release, { tanodb , "0.1.0"},
                 [tanodb,
                  cuttlefish,
                  cowboy,
                  {leveled, load},
                  {lz4, load},
                  jsx,
                  sasl]},

At this point in time, to be able to compile leveled on Erlang 20.3, we need to add an override to remove the warnings_as_errors option in erl_opts:

{override, leveled,
        [{erl_opts, [{platform_define, "^1[7-8]{1}", old_rand},
                {platform_define, "^R", old_rand},
                {platform_define, "^R", no_sync}]}]}

The code for apps/tanodb/src/tanodb_kv_leveled.erl:

-module(tanodb_kv_leveled).
-export([new/1, get/3, put/4, delete/3, keys/2, dispose/1, delete/1,
                 is_empty/1, foldl/3]).

-include_lib("leveled/include/leveled.hrl").

-record(state, {bookie, base_path}).

new(#{partition := Partition, data_path := DataPath}) ->
        Path = filename:join([DataPath, "leveled", integer_to_list(Partition)]),
        {ok, Bookie} = leveled_bookie:book_start(Path, 2000, 500000000, none),
        State = #state{bookie=Bookie, base_path=Path},
        {ok, State}.

put(State=#state{bookie=Bookie}, Bucket, Key, Value) ->
        R = leveled_bookie:book_put(Bookie, Bucket, Key, Value, []),
        {R, State}.

get(State=#state{bookie=Bookie}, Bucket, Key) ->
        K = {Bucket, Key},
        Res = case leveled_bookie:book_get(Bookie, Bucket, Key) of
                          not_found -> {not_found, K};
                          {ok, Value} -> {found, {K, Value}}
                  end,
        {Res, State}.

delete(State=#state{bookie=Bookie}, Bucket, Key) ->
        R = leveled_bookie:book_delete(Bookie, Bucket, Key, []),
        {ok, State}.

keys(State=#state{bookie=Bookie}, Bucket) ->
        FoldHeadsFun = fun(_B, K, _ProxyV, Acc) -> [K | Acc] end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                        {foldheads_bybucket,
                                                                ?STD_TAG,
                                                                Bucket,
                                                                all,
                                                                FoldHeadsFun,
                                                                true, true, false}),
        Keys = FoldFn(),
        {Keys, State}.

is_empty(State=#state{bookie=Bookie}) ->
        FoldBucketsFun = fun(B, Acc) -> [B | Acc] end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie,
                                                                                                           {binary_bucketlist,
                                                                                                                ?STD_TAG,
                                                                                                                {FoldBucketsFun, []}}),
        IsEmpty = case FoldFn() of
                                  [] -> true;
                                  _ -> false
                          end,
        {IsEmpty, State}.

dispose(State=#state{bookie=Bookie}) ->
        ok = leveled_bookie:book_close(Bookie),
        {ok, State}.

delete(State=#state{base_path=Path}) ->
        R = remove_path(Path),
        {R, State}.

foldl(Fun, Acc0, State=#state{bookie=Bookie}) ->
        FoldObjectsFun = fun(B, K, V, Acc) -> Fun({{B, K}, V}, Acc) end,
        {async, FoldFn} = leveled_bookie:book_returnfolder(Bookie, {foldobjects_allkeys,
                                                                                                                                ?STD_TAG,
                                                                                                                                {FoldObjectsFun, Acc0},
                                                                                                                                true}),
        AccOut = FoldFn(),
        {AccOut, State}.

% private functions

sub_files(From) ->
        {ok, SubFiles} = file:list_dir(From),
        [filename:join(From, SubFile) || SubFile <- SubFiles].

remove_path(Path) ->
        case filelib:is_dir(Path) of
                false ->
                        file:delete(Path);
                true ->
                        lists:foreach(fun(ChildPath) -> remove_path(ChildPath) end,
                                                  sub_files(Path)),
                        file:del_dir(Path)
        end.

Trying it

From the user perspective nothing changed other than the fact that the data will persist between restarts.

To test it redo the "Trying it" sections from the Handoff and Coverage Calls chapters.

Creemos en la Web: Nombrando cosas

En la sección anterior (Creemos en la Web: Una potente calculadora) aprendimos a usar javascript como una calculadora, probemos usarla para eso convirtiendo de grados Celsius a Fahrenheit.

En la mayoría de los países se usan los grados centígrados para temperatura, pero en algunos otros se usan los grados Fahrenheit, buscando en google como es la formula de conversión, google me dice esto:

°F = °C x 1.8 + 32

Donde °F significa "el resultado de la conversión, grados fahrenheit" y °C significa "el valor en grados centígrados a convertir".

Como podemos ver, la formula "nombra" valores que van a variar según que queremos convertir, probemos calcular cuanto es 36 grados centígrados a fahrenheit:

36 * 1.8 + 32
< 96.8

El código es bastante similar, pero tiene un par de problemas:

Primero, si nos encontramos con la expresión 36 * 1.8 + 32 en el medio de otro código, como sabemos que partes son "parámetros", es decir, valores que podemos cambiar y cuales son valores fijos de la formula.

Segundo, como sabemos formula de que es? puede ser cualquier calculo.

Tercero, si queremos usar esa formula muchas veces para distintos valores, tenemos que escribir la formula cada vez? Osea que tenemos que recordarla siempre o buscarla siempre? Esto puede hacer que en algún momento nos equivoquemos y la escribamos mal o si necesitamos hacer un cambio tenemos que buscar todas las veces que la usamos y cambiarla.

Todos estos son indicios de lo mismo, nos gustaría ponerle un nombre a la formula y cada vez que la queramos usar simplemente mencionamos el nombre y le proveemos los valores que requiera. En este caso el nombre podría ser "centigradosAFahrenheit".

... silencio ...

Te preguntas porque ese nombre todo junto?

Resulta que en javascript podemos nombrar cualquier cosa siempre que el nombre cumpla con un par de condiciones:

  • Tiene que empezar con una letra, minúscula o mayúscula del alfabeto ingles
  • Luego puede tener cualquier combinación de letras minúsculas, mayúsculas o números

Nombres validos:

  • A
  • b
  • Ab
  • aB
  • a1
  • diaDelMes
  • parametro9

Nombres inválidos:

  • 1a
    • empieza con numero
  • a#
    • tiene un carácter que no es ni una letra ni un numero
  • centígrados a fahrenheit
    • tiene espacios y tilde, no esta permitido
  • año
    • tiene ñ

Otro detalle es que en javascript, las formulas se llaman funciones, como en matemáticas, aunque las funciones de javascript son mas generales que las funciones en matemáticas, funcionan de una manera similar:

  • Una función tiene un nombre
  • Una función recibe cero o mas valores de entrada o parámetros
  • Una función devuelve un valor de salida

Veamos como definir una función bien simple en javascript, una función que no recibe ningún parámetro y siempre devuelve el número 7:

function siete() {
    return 7;
}

Vamos por partes:

Para definir una función en javascript, empezamos escribiendo la palabra "function", que significa función en ingles.

Luego indicamos el nombre de la función, en este caso "siete".

Luego viene la lista de parámetros entre paréntesis, separados por coma si hay mas de uno, en este caso no necesitamos ningún parámetro, así que simplemente abrimos y cerramos los paréntesis.

Luego viene "el cuerpo" de la función, que es donde hacemos los cálculos necesarios, cada calculo se separa del siguiente con un punto y coma ";".

Cuando queremos terminar de ejecutar la función y devolver un valor lo indicamos empezando la linea con la palabra "return" (retornar en ingles) y el valor que queremos devolver, en este caso el numero 7.

Como usamos nuestra primera función? la nombramos y le indicamos los parámetros que le queremos pasar, en este caso no hay parámetros así que de nuevo es simplemente abrir y cerrar paréntesis.

siete()
< 7

Volviendo al caso de la formula de conversión, tenemos un parámetro que es el valor en centígrados que queremos convertir.

Como ese parámetro va a tomar distintos valores para cada conversión, le vamos a poner un nombre, propongo ponerle "gradosC".

Veamos de definir nuestra función:

function centigradosAFahrenheit(gradosC) {
    return gradosC * 1.8 + 32;
}

Como veras seguimos la misma estructura que antes, la palabra "function", seguida del nombre de la función a definir, seguida de la lista de parámetros entre paréntesis, en este caso uno solo, llamado "gradosC", seguido del cuerpo de la función entre llaves.

El cuerpo de la función tiene una sola linea que "retorna" el resultado de calcular gradosC * 1.8 + 32.

Como veras cuando queremos usar un parámetro simplemente lo nombramos y javascript reemplaza el valor que se le asigno en la llamada en ese lugar, por lo que si llamamos a la función con el valor 36:

centigradosAFahrenheit(36)

Nos devuelve:

< 96.8

Lo que hace javascript es reemplazar el valor del parámetro en esta llamada dentro del cuerpo de la función, así que:

return gradosC * 1.8 + 32;

En esta llamada se convierte en:

return 36 * 1.8 + 32;

Vamos viendo que nombrar cosas hace todo mas legible y reusable, dentro de una función podemos nombrar cualquier valor si hace las cosas mas claras.

En este ejemplo es mas o menos claro, pero si quisiéramos ser mas explícitos podríamos ponerle un nombre al resultado como la formula inicial:

°F = °C x 1.8 + 32

Eso lo hacemos con otra palabra especial, cuando queremos nombrar una función nueva usamos la palabra "function", cuando queremos nombrar un parámetro simplemente lo nombramos en la lista de parámetros, pero cuando queremos nombrar un valor en el cuerpo de una función, tenemos que usar la palabra "let", una palabra que podríamos traducir como "sea", veamos su uso así queda mas claro:

function centigradosAFahrenheit1(gradosC) {
    let gradosF = gradosC * 1.8 + 32;
    return gradosF;
}

La linea:

let gradosF = gradosC * 1.8 + 32;

Podríamos traducirla como "sea gradosF igual a gradosC * ...".

En la siguiente linea nombramos gradosF, que es reemplazado por el valor que se le asigno en la linea anterior.

Con esto aprendimos que en javascript hay al menos 3 cosas que podemos nombrar:

  • Funciones: una forma de reusar pedazos de código sin repetirnos
  • Parámetros: una forma de indicar partes del código que pueden variar
  • Variables: una forma de nombrar cálculos intermedios sin repetirnos

Estas 3 formas de nombrar nos permiten hacer nuestro código mas claro, hay un dicho muy conocido en la programación que dice:

Hay solo dos cosas difíciles en la programación: nombrar cosas e invalidar caches.

Ya aprendimos a nombrar cosas, lo difícil es ponerle el nombre indicado a cada cosa para que luego cuando se lea el código de nuevo quede clara la intención.

Sobre caches vamos a aprender eventualmente, alguna vez cuando una pagina no funcionaba bien alguien te habrá dicho que limpies el cache de la computadora, esa es la razón :)

Riak Core Tutorial Part 8: HTTP API

The content of this chapter is in the 06-http-api branch.

https://gitlab.com/marianoguerra/tanodb/tree/06-http-api

How it Works

We are adding a simple HTTP API to our system, it will run on all nodes and allow us to interact with it from the outside.

We will use the Cowboy 2.0 HTTP Server.

Implementing it

We need to add cowboy as a dependency on rebar.config and tanodb.app.src.

Then in tanodb_app.erl we need to start the HTTP API:

setup_http_api() ->
  Dispatch = cowboy_router:compile([{'_', [{"/kv/:bucket/:key", tanodb_h_kv, []}]}]),

  HttpPort = application:get_env(tanodb, http_port, 8080),
  HttpAcceptors = application:get_env(tanodb, http_acceptors, 100),
  HttpMaxConnections = application:get_env(tanodb, http_max_connections, infinity),

  lager:info("Starting HTTP API at port ~p", [HttpPort]),

  {ok, _} = cowboy:start_clear(tanodb_http_listener,
    [{port, HttpPort},
     {num_acceptors, HttpAcceptors},
     {max_connections, HttpMaxConnections}],
    #{env => #{dispatch => Dispatch}}),

  ok.

We get the configuration from the environment, which is set by cuttlefish.

The API handler module tanodb_h_kv's main code:

init(ReqIn=#{method := <<"GET">>}, State) ->
    {Bucket, Key} = bindings(ReqIn),
    reply(ReqIn, State, tanodb:get(Bucket, Key));
init(ReqIn=#{method := <<"POST">>}, State) ->
    {Bucket, Key} = bindings(ReqIn),
    {ok, Value, Req1} = read_all_body(ReqIn),
    reply(Req1, State, tanodb:put(Bucket, Key, Value));
init(ReqIn=#{method := <<"DELETE">>}, State) ->
    {Bucket, Key} = bindings(ReqIn),
    reply(ReqIn, State, tanodb:delete(Bucket, Key)).

Trying it

Single Node

We are going to first test it on a single node.

Get key k1 in bucket b1, which doesn't exist yet:

curl -X GET  http://localhost:8098/kv/b1/k1
{
  "replies": [
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Put key k1 in bucket b1 with content hi there:

curl -X POST  http://localhost:8098/kv/b1/k1 -d 'hi there'
{
  "replies": [
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Get key k1 in bucket b1, which now exists:

curl -X GET  http://localhost:8098/kv/b1/k1
{
  "replies": [
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088",
      "value": "hi there"
    },
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104",
      "value": "hi there"
    },
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120",
      "value": "hi there"
    }
  ]
}

Delete key k1 in bucket b1:

curl -X DELETE  http://localhost:8098/kv/b1/k1
{
  "replies": [
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "node": "tanodb@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Get key k1 in bucket b1, which shouldn't exist anymore:

curl -X GET  http://localhost:8098/kv/b1/k1
{
  "replies": [
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb@127.0.0.1",
      "ok": false,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Cluster

We are going to test it on a cluster now, notice that the port changes, we are sending each request to a different node.

You can see each node's port on the logs at startup:

[info] Starting HTTP API at port 8198

Get key k1 in bucket b1, which doesn't exist yet:

curl -X GET  http://localhost:8198/kv/b1/k1

Notice the node name on the partition field, it may change for you depending on the state of handoff or how vnodes were distributed.

{
  "replies": [
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb2@127.0.0.1",
      "ok": false,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": false,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": false,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Put key k1 in bucket b1 with content hi there:

curl -X POST  http://localhost:8298/kv/b1/k1 -d 'hi there'
{
  "replies": [
    {
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120"
    },
    {
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "node": "tanodb2@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088"
    }
  ]
}

Get key k1 in bucket b1, which now exists:

curl -X GET  http://localhost:8398/kv/b1/k1
{
  "replies": [
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104",
      "value": "hi there"
    },
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120",
      "value": "hi there"
    },
    {
      "bucket": "b1",
      "key": "k1",
      "node": "tanodb2@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088",
      "value": "hi there"
    }
  ]
}

Delete key k1 in bucket b1:

curl -X DELETE  http://localhost:8198/kv/b1/k1
{
  "replies": [
    {
      "node": "tanodb2@127.0.0.1",
      "ok": true,
      "partition": "1301649895747835411525156804137939564381064921088"
    },
    {
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "node": "tanodb1@127.0.0.1",
      "ok": true,
      "partition": "1255977969581244695331291653115555720016817029120"
    }
  ]
}

Get key k1 in bucket b1, which shouldn't exist anymore:

curl -X GET  http://localhost:8298/kv/b1/k1
{
  "replies": [
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": false,
      "partition": "1278813932664540053428224228626747642198940975104"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb1@127.0.0.1",
      "ok": false,
      "partition": "1255977969581244695331291653115555720016817029120"
    },
    {
      "bucket": "b1",
      "error": "not_found",
      "key": "k1",
      "node": "tanodb2@127.0.0.1",
      "ok": false,
      "partition": "1301649895747835411525156804137939564381064921088"
    }
  ]
}

Creemos en la Web: Una potente calculadora

En :doc:`datos-con-javascript` aprendimos sobre los distintos tipos de datos en javascript (normalmente abreviado js).

System Message: ERROR/3 (<string>, line 1); backlink

"datos-con-javascript" slug doesn't exist.

Ahora vamos a aprender como realizar operaciones sobre algunos de esos datos, lo suficiente para tener una muy potente calculadora a nuestra disposición.

Ya que mencionamos la calculadora, empecemos por los que están disponibles en una.

Operadores Matemáticos

Suma y resta

Para números enteros (normalmente se le dicen int/ints) y decimales (float/floats):

Con enteros el resultado es otro entero:

2 + 3
< 5
2 - 3
< -1

Como veras si ponemos el menos delante de un numero indicamos que es negativo:

2 - -3
< 5

También podemos poner un mas, aunque no hace mucho efecto:

2 - +3
< -1

Cuando la operación mezcla enteros y decimales, el resultado "se promueve" a decimal:

2 - 3.5
< -1.5

Podemos tener mas de una operación en la misma linea:

2 - 3.5 + 4 - -2
< 4.5

Multiplicación y división

El símbolo de la multiplicación y división son distintos a los de la calculadora o los que escribimos a mano, la multiplicación es * y la división es /, veamos algunos ejemplos:

2 * 3
< 6
10 / 4
< 2.5

Fijate que la división devuelve decimales aunque los números sean enteros.

Mezclemos un poco:

2 * 3 / 4
< 1.5

Dividiendo por cero nos da un resultado raro:

10 / 0
< Infinity

Y su par:

10 / -0
< -Infinity

Precedencia

Vimos suma y resta separado de multiplicación y división porque esos operadores tienen distinta precedencia, es decir, se evalúan en un orden establecido, veamos un ejemplo, si te digo que adivines el resultado de:

2 + 3 * 4

Cual crees que es?

  • 14
  • 20

El resultad es 14, porque la multiplicación se "evalua" antes que la suma, si no especificamos explícitamente la precedencia con paréntesis, javascript "inserta" los paréntesis según cierto orden preestablecido.

2 + 3 * 4

Es lo mismo que

2 + (3 * 4)

Yo siempre prefiero poner los paréntesis para que quede clara la precedencia aun cuando no es necesario, esto va a ser aun mas importante cuando aprendamos a un mas operadores, si de hecho queremos el otro resultado, simplemente usamos paréntesis para forzar el orden deseado:

(2 + 3) * 4
< 20

Resto de división

Si en lugar del resultado de la división queremos saber el resto, usamos el operador %:

10 % 3
< 1

Operadores de comparación

Con los operadores que vimos hasta ahora ya tenemos una potente calculadora, pero si recordas el tipo bool del capítulo anterior, recordaras que dije que es el resultado de comparaciones y operaciones lógicas, ahora vamos a ver algunas de ellas.

Igual y desigual

Lo mas fácil que podemos comparar es si dos cosas son iguales, pero como todo en la programación, las cosas son un poco mas complicadas de lo necesario, vamos por partes:

Comparando por igualdad:

1 es igual a si mismo

1 == 1
< true

1 no es distinto a si mismo

1 != 1
< false
1 == 2
< false

1 no es distinto a si mismo

1 != 2
< true

Hasta acá todo bien, pero ya sabemos que en javascript hay dos tipos para indicar ausencia de datos, null y undefined, hace mucho tiempo, intentando hacernos un favor, los creadores de javascript decidieron que ciertas cosas eran iguales cuando claramente no lo son:

null == undefined
< true
"1" == 1
< true

Si, viste bien, el texto con el contenido "1", igual al numero 1, eso les pareció una ayuda, pero el 99.9% de las veces termina siendo un problema, razón por la cual introdujeron otros dos operadores de igualdad mas estrictos, veamos:

null === undefined
< false
"1" === 1
< false
null !== undefined
< true
"1" !== 1
< true

Ahora te doy un consejo, que mas que consejo es orden, siempre usa === y !==, porque cuando veas código usando las versiones mas flexibles, no sabes si hay un posible error en ese código o si la persona realmente quiere hacer una comparación mas flexible.

Mayores y Menores

Si queremos saber si un numero es menor o mayor a otro usamos los operadores de comparación correspondientes:

a > b
es a mayor que b?
a >= b
es a mayor o igual que b?
a < b
es a menor que b?
a <= b
es a menor o igual que b?
1 < 2
< true
1 <= 1
< true
1 > 2
< false
1 >= 1
< true

Precedencia

La siguiente linea:

1 + 2 * 3 < 3 * 4 + 5
< true

Es equivalente a:

(1 + (2 * 3)) < ((3 * 4) + 5)
< true

Pero antes de seguir intentando memorizarte el orden, mas fácil es siempre usar paréntesis :)

Operadores logicos

En la vida real, cuando tomamos decisiones, usualmente decimos cosas como:

Si el precio del tomate es menor que 5 y el precio de la lechuga es menor
que 6 o el precio del ajo es mayor a 7, entonces...

En esa expresión ya sabemos como expresar los números y las comparaciones, lo que nos falta expresar son los "y" y los "o", vamos a por ellos.

Conjunción (y)

La conjunción es cuando el resultado de una expresión es cierta/verdadera/true si ambas partes son verdaderas. En javascript el símbolo que usamos para expresarla es &&, veamos todos los casos de este operador en una tabla:

Operación Resultado
true && true true
true && false false
false && true false
false && false false

Como podemos ver, solo es verdadero si ambos lados son verdaderos, veamoslo en javascript

true && true
< true
true && false
< false
false && true
< false
false && false
< false

En ambos lados podemos poner cualquier expresión que evalúe a true o false:

1 === 1 && 1 !== 1
< false
1 === 1 && 1 !== 2
< true

Como con los otros operadores, podemos tener mas de uno:

1 === 1 && 1 !== 2 && 2 === 2
< true

Disyunción (o)

La disyunción es cuando el resultado de una expresión es cierta/verdadera/true si al menos una de las partes son verdaderas.

En javascript el símbolo que usamos para expresarla es ||, veamos todos los casos de este operador en una tabla:

Operación Resultado
true || true true
true || false true
false || true true
false || false false

Veamoslo en javascript

true || true
< true
true || false
< true
false || true
< true
false || false
< false

En ambos lados podemos poner cualquier expresión que evalúe a true o false:

1 === 1 || 1 !== 1
< false
1 === 1 || 1 !== 2
< true

Como con los otros operadores, podemos tener mas de uno:

1 === 1 || 1 !== 2 || 2 === 2
< true

Negación (no)

En español, si decimos "no es verdadero", nos referimos a que es falso, y al revés, "no es falso", indica que es verdadero. En lógica este operador se llama "not" (no en ingles), en js el operador es ! y se pone delante de un valor para "negarlo":

!true
< false
!false
< true

Podemos negar cualquier expresión, si es mas que un valor, lo ponemos entre paréntesis:

!(1 === 1) || (1 === 1)
< true
!(!(1 === 1) || (1 === 1))
< false
!!true
< true

Sobre valores símil falsos y cortocircuitos

Si te gusta romper todo lo que cae en tus manos como yo, se te habrá ocurrido "que pasa si pongo valores raros en esos operadores", bueno, javascript intenta ayudarnos, pero a veces sus ayudas terminan siendo poco útiles, veamos algunos ejemplos.

Si negamos dos veces un booleano, nos da el mismo valor:

!!true
< true

Que pasa si negamos otros valores? veamos:

!0
< true
!1
< false

Como le pedimos que niegue un valor y js espera un booleano, al darle otra cosa lo "convierte" (coherse en ingles) a booleano siguiendo ciertas reglas, todos los tipos tienen un valor que evalúa a falso y todos los otros a verdadero, si negamos dos veces cualquier valor obtenemos su valor booleano, por lo que podemos decir que cero es "falsy" (es una expresión en ingles que podríamos traducir como "similar a false") y cualquier otro numero es verdadero:

!!0
< false
!!1
< true
!!-1
< true
!!42
< true
!!""
< false
!!"0"
< true
!![]
< true
!!{}
< true
!!null
< false
!!undefined
< false

Entonces podemos decir que 0, el texto vacío, null y undefined, cuando son evaluados en operaciones que esperan un valor booleano, actúan como false, todos los otros, como true.

Y los cortocircuitos?

En los operadores && y || hay casos donde viendo el lado izquierdo de la operación, javascript ya sabe cual es el resultado, veamos de nuevo las tablas de ambos operadores:

Operación Resultado
true && true true
true && false false
false && true false
false && false false

Si el lado izquierdo es false, el resultado va a ser false, por lo cual para que el código se ejecute mas rápido, javascript no evalúa el lado derecho y devuelve directamente false.

Operación Resultado
true || true true
true || false true
false || true true
false || false false

Si el lado izquierdo es true, el resultado va a ser true, por lo cual el lado derecho no se ejecuta y devuelve directamente true.

Esto no es importante aun porque todavía no vimos funciones, pero imaginate que javascript encuentra la expresión:

false && lanzarMisil()

Por la lógica de "cortocircuito", el misil no se va a lanzar, ya que la función no va a ser evaluada.

Riak Core Tutorial Part 7: Coverage Calls

The content of this chapter is in the `05-coverage` branch.

https://gitlab.com/marianoguerra/tanodb/tree/05-coverage

How it Works

Since bucket and key are hashed together to decide to which vnode a request will go it means that the keys for a given bucket may be distributed in multiple vnodes, and in case you are running in a cluster this means your keys are distributed in multiple physical nodes.

This means that to list all the keys from a bucket we have to ask all the vnodes for the keys on a given bucket and then put the responses together and return the set of all responses.

For this Riak Core provides something called coverage calls, which are a way to handle this process of running a command on all vnodes and gathering the responses.

In this chapter we are going to implement the tanodb:keys(Bucket) function using coverage calls.

In this case we call tanodb_coverage_fsm:start({keys, Bucket}, Timeout), which is a new module, it implements a behavior called riak_core_coverage_fsm, short for riak_core_coverage finite state machine, it implements some predefined callbacks that are called on different states of a finite state machine.

The start function calls tanodb_coverage_fsm_sup:start_fsm([ReqId, self(), Request, Timeout]) which starts a supervisor for this new process.

When we start the fsm with a command {keys, Bucket} and a timeout in milliseconds, it starts a supervisor that starts the finite state machine process, it first calls the init function which initializes the state of the process and returns some information to riak_core so it knows what kind of coverage call we want to do, then riak_core calls the handle_coverage function on each vnode and with each response it calls process_result in our process.

When all the results are received or if an error happens (such as a timeout) it will call the finish callback there we send the results to the calling process which is waiting for it.

The handle_coverage implementation is really simple, it uses the ets:match/2 function to match against all the entries with the given bucket and returns the key from the matched results.

You can read more about ets match specs in the match spec chapter on the Erlang documentation.

Implementing it

Code in tanodb.erl is really simple:

keys(Bucket, Opts) ->
    Timeout = maps:get(timeout, Opts, ?TIMEOUT),
    tanodb_coverage_fsm:start({keys, Bucket}, Timeout).

In tanodb_vnode.erl we need to implement the handle_coverage callback:

handle_coverage({keys, Bucket}, _KeySpaces, {_, RefId, _},
                State=#state{kv_state=KvState}) ->
    {Keys, KvState1} = tanodb_kv_ets:keys(KvState, Bucket),
    {reply, {RefId, Keys}, State#state{kv_state=KvState1}};

We add two new modules:

tanodb_coverage_fsm
The FSM implementation for the coverage call.
tanodb_coverage_fsm_sup
The supervisor for the FSM processes.

We also add the tanodb_coverage_fsm_sup to the tanodb_sup supervisor tree.

Trying it

Nums = lists:seq(1, 10).
Buckets = lists:map(fun (N) -> list_to_binary("bucket-" ++ integer_to_list(N)) end,
Nums).
Keys = lists:map(fun (N) -> list_to_binary("key-" ++ integer_to_list(N)) end, Nums).

GenValue = fun (Bucket, Key) -> [{bucket, Bucket}, {key, Key}] end.

lists:foreach(fun (Bucket) ->
        lists:foreach(fun (Key) ->
                Val = GenValue(Bucket, Key),
                tanodb:put(Bucket, Key, Val)
        end, Keys)
end, Buckets).

{ok, Items} = tanodb:keys(<<"bucket-1">>).
[{Partition, Node, Keys} || {Partition, Node, Keys} <- Items, Keys =/= []].
[{296867520082839655260123481645494988367611297792,
  'tanodb@127.0.0.1', [<<"key-10">>]},
 {365375409332725729550921208179070754913983135744,
  'tanodb@127.0.0.1', [<<"key-4">>]},
 {137015778499772148581595453067151533092743675904,
  'tanodb@127.0.0.1', [<<"key-8">>]},
 {707914855582156101004909840846949587645842325504,
  'tanodb@127.0.0.1', [<<"key-9">>]},
 {45671926166590716193865151022383844364247891968,
  'tanodb@127.0.0.1', [<<"key-2">>]},
 {753586781748746817198774991869333432010090217472,
  'tanodb@127.0.0.1', [<<"key-9">>]},
 {274031556999544297163190906134303066185487351808,
  'tanodb@127.0.0.1', [<<"key-10">>]},
 {822094670998632891489572718402909198556462055424,
  'tanodb@127.0.0.1', [<<"key-5">>]},
 {319703483166135013357056057156686910549735243776,
  'tanodb@127.0.0.1', [<<"key-4">>,<<"key-10">>]},
 {342539446249430371453988632667878832731859189760,
  'tanodb@127.0.0.1', [<<"key-4">>]},
 {68507889249886074290797726533575766546371837952,
  'tanodb@127.0.0.1', [<<"key-2">>]},
 {799258707915337533392640142891717276374338109440,
  'tanodb@127.0.0.1', [<<"key-5">>]},
 {91343852333181432387730302044767688728495783936,
  'tanodb@127.0.0.1', [<<"key-2">>]},
 {730750818665451459101842416358141509827966271488,
  'tanodb@127.0.0.1', [<<"key-9">>]},
 {159851741583067506678528028578343455274867621888,
  'tanodb@127.0.0.1', [<<"key-8">>]},
 {182687704666362864775460604089535377456991567872,
  'tanodb@127.0.0.1', [<<"key-8">>]},
 {844930634081928249586505293914101120738586001408,
  'tanodb@127.0.0.1', [<<"key-5">>]},
 {867766597165223607683437869425293042920709947392,
  'tanodb@127.0.0.1', [<<"key-3">>]},
 {890602560248518965780370444936484965102833893376,
  'tanodb@127.0.0.1', [<<"key-3">>]},
 {1050454301831586472458898473514828420377701515264,
  'tanodb@127.0.0.1', [<<"key-6">>]},
 {913438523331814323877303020447676887284957839360,
  'tanodb@127.0.0.1', [<<"key-3">>]},
 {1118962191081472546749696200048404186924073353216,
  'tanodb@127.0.0.1', [<<"key-7">>,<<"key-1">>]},
 {1164634117248063262943561351070788031288321245184,
  'tanodb@127.0.0.1', [<<"key-7">>]},
 {1027618338748291114361965898003636498195577569280,
  'tanodb@127.0.0.1', [<<"key-"...>>]},
 {1096126227998177188652763624537212264741949407232,
  'tanodb@127.0.0.1', [<<...>>]},
 {1073290264914881830555831049026020342559825461248,
  'tanodb@127.0.0.1', [...]},
 {1141798154164767904846628775559596109106197299200,
  'tanodb@127.0.0.1',...}]

Creemos en la Web: Datos con Javascript

En el capítulo anterior Creemos en la Web: Un poco de lógica a la vista usamos un poco de javascript pero sin una introducción formal, ya es tiempo de conocerlo un poco mejor, empezando con la forma de expresar distintos tipos de datos, esto nos va a permitir definir el estado inicial de nuestra aplicación.

Antes de empezar de lleno te recomiendo que escribas los ejemplos que voy poniendo así te vas acostumbrando a programar, es muy común pensar que solo leyendo uno entiende las cosas, pero es muy fácil de darse cuenta que no es tan así cuando intentamos escribirlo por nuestra cuenta y empezamos a notar detalles importantes que se nos pasaron de largo al leerlo.

Para poder probar hay dos formas, la primera es abrir la sección de consola interactiva en las herramientas de desarrollo de tu navegador.

Las herramientas de desarrollo se abren apretando la tecla F12 en Firefox o Chrome, esto nos va a abrir un panel en la parte inferior de la pantalla, la cual tiene múltiples secciones, la que vamos a usar hoy es la que normalmente es la tercera de izquierda a derecha, llamada "Consola", hace click en el tab "Consola" o "Console" según el idioma de tu navegador.

En la parte inferior hay un símbolo > o >>, si hacemos click a la derecha va a aparecer un cursor, ahí podemos escribir javascript, el cual se va a ejecutar cuando apretemos enter y nos va a mostrar el resultado (o un error).

La otra forma es usar alguna consola interactiva online, una conocida es repl.it, hace click y debería abrirte una pagina donde podes escribir en el panel del medio y ejecutarlo apretando la tecla Control y Enter a la vez, el resultado debería aparecer en el panel de la derecha.

Acá hay un video mostrando como usarlo:

Ya tenemos lo que necesitamos para empezar.

Números Decimales

Un tipo de dato muy usado, tenemos dos tipos, los enteros (integer en ingles), son números sin coma decimal, veamos algunos:

Escribiendo lo siguiente:

1

Si apretamos enter en la consola del navegador o Ctrl+Enter en repl.it el resultado que nos va a devolver es:

< 1

Nada mágico, decimos que javascript evaluó el código 1 y el resultado fue 1, aunque no lo creas eso es un programa valido, aunque no uno muy útil.

Veamos otros números, de ahora en mas voy a poner el código y el resultado, se entiende que tenes que escribirlo y apretar enter o Ctrl+enter según donde estés escribiendo el código para ver el resultado.

2
< 2
0
< 0
42
< 42

También se pueden números negativos:

-42
< -42

Números grandes

1234567890
< 1234567890

Números Decimales

También podemos escribir números con decimales, en programación los vas a escuchar nombrar como "float", "floating point number", o "double", en lugar de una coma se usa un punto:

0.5
< 0.5
10.5
< 10.5
-0.5
< -0.5

Texto

Otro tipo de dato muy importante es el texto, como vas a ir viendo a los programadores les gusta poner nombres raros a las cosas y nunca decidir cambiarlos, por mas que la razón del nombre ya se haya olvidado.

En este caso, al tipo de dato texto en programación se le suele llamar cadena de texto, en ingles "string".

En javascript el texto es cualquier cosa envuelta en comillas simples ' o dobles, ", a la computadora le da igual cual uses.

Empecemos con el texto mas simple, texto vacío:

""
< ""

En Firefox cuando escribo con comillas simples me lo muestra de vuelta en comillas dobles, así que parece que tiene una preferencia :)

''
< ""

El texto mas común que vas a encontrar en introducciones a programación:

'Hola mundo!'
< "Hola mundo!"

Si queremos tener comillas dentro del texto, tenemos que "escaparlas" con una barra invertida antes de la comilla si es del mismo tipo que estamos usando para envolver el texto, así la computadora sabe que no se termina el texto aun, esta es una buena razón para usar un tipo de comillas sobre el otro, para evitar tener que escapar las comillas internas:

'Esto es "texto"'
< "Esto es \"texto\""

Si queremos usar el mismo tipo de comillas las tenemos que escapar:

"Esto es \"texto\""
< "Esto es \"texto\""

Igual para comillas simples:

"Esto es 'texto'"
< "Esto es 'texto'"
'Esto es \'texto\''
< "Esto es 'texto'"

Verdadero y Falso

En programación se toman muchas decisiones, esas decisiones se toman según condiciones, las cuales evalúan a verdadero o falso, y según el resultado decidimos hacer una cosa, otra, o ninguna.

Debido a que esto es algo muy común, existe un tipo de dato muy simple, que se llama Lógico pero lo vas a encontrar usualmente mencionado como booleano (pronunciado buleano) o directamente en ingles, bool (pronunciado bul), en honor a George Boole

Hay solo dos valores para este tipo de datos:

Verdadero:

true
< true

Falso:

false
< false

Indicando ausencia de datos

Como hacemos si queremos indicar que a un dato no lo tenemos?

Un tal Tony Hoare se pregunto lo mismo en 1965 y tuvo una idea, un tipo de dato al cual luego iba a llamar "El error de los mil millones de dólares" , este es el tipo de dato nulo, el cual tiene un solo valor:

null
< null

El problema surge cuando pensamos que vamos a recibir un número, texto u otro tipo de dato y alguien nos envía un null para indicarnos que no tiene ningún valor, nosotros operamos sobre ese valor asumiendo que tiene un valor y lo que obtenemos a cambio es un error. Así que primer consejo, intenta minimizar la cantidad de veces que usas este tipo de datos, aunque muchas veces no quede alternativa.

Hasta aquí llegamos y ya aprendimos todos los tipos de datos simples en javascript, ahora pasemos a los tipos de datos "compuestos", es decir, estos tipos de datos contienen otros datos.

Lista

En nuestro ejemplo del capitulo anterior teníamos una lista de tareas, cada tarea era un valor de tipo texto.

Una lista se define rodeando sus elementos entre corchetes [ para empezar la lista y ] para terminarla.

Por razones históricas el tipo de dato lista también suele llamarse en ingles "array".

Empecemos por la lista mas simple, una lista sin elementos:

[]

Firefox me lo muestra así:

< Array []

Una lista de un elemento:

[1]
< Array [ 1 ]

Una lista de dos elementos, separamos cada elemento con una coma ,:

[1, true]
< Array [ 1, true ]

Como veras una lista puede contener cualquier tipo de dato, no hace falta que todos sean del mismo tipo, veamos un caso extremo con todos los tipos de datos que aprendimos hasta ahora, te desafío a escribirlo sin cometer un error al primero intento, yo cometí uno (pista: me olvide de cerrar un corchete :).

[-1, 0, 1, 42, 0.5, true, false, null, "", 'hola', [], [["si, listas dentro de listas"]]]

Firefox me lo muestra indicándome que tiene 12 elementos y me muestra algunos, no todos, no te preocupes, todos los elementos están ahí, si hago click en la flecha > me expande los elementos así los puedo explorar:

< Array(12) [ -1, 0, 1, 42, 0.5, true, false, null, "", "hola", … ]

Objeto

Felicitaciones, llegamos al ultimo tipo de dato de javascript, el objeto, llamado de muchas formas, en otro lado lo veras como "map", "hash", "record", en javascript se lo suele llamar objeto, así que vamos a seguir nombrandolo así.

Un objeto es un tipo de dato que nos permite ponerle nombres a los valores que contiene, entonces si queremos, por ejemplo, tener un valor que represente un personaje en una serie y queremos tener su nombre, su tipo y su color, podemos hacerlo con un objeto, pero no nos adelantemos, empecemos con el objeto mas simple, uno que no tiene ningún valor, sí, aunque no lo creas es muy útil.

{}

Por razones que aprenderemos mas adelante, firefox se piensa que estoy intentando hacer otra cosa y me devuelve:

< undefined

Quizás cuando pruebes esto ya no se comporte así, Chrome no se confunde y devuelve:

< {}

Te preguntaras que es ese undefined que devolvió?

Es un tipo de dato que no mencione hasta ahora porque es un valor que representa la ausencia de valores, no es como null que es un valor que dice "no hay valor", undefined es un poco mas confuso y abstracto y es usado cuando una operación no devuelve ningún valor, o cuando por ejemplo intentamos obtener un elemento en una lista vacía o un campo que no existe en un objeto, en ese caso null no es útil porque ese campo no esta definido como null, simplemente no esta definido, en esos casos obtenemos a su primo: undefined.

No hace falta que entiendas el significado de undefined aun, ya vas a encontrartelo mas seguido de lo deseado.

Sigamos con un objeto con un solo campo:

{"nombre": "Bob"}
< {nombre: "Bob"}

Para tener mas de un campo separamos los pares clave valor (los recordaras de CSS) con comas:

{"nombre": "Bob", "color": "amarillo", "tipo": "esponja"}
< {nombre: "Bob", color: "amarillo", tipo: "esponja"}

Listo! ya sabes todos los tipos de datos necesarios para programar en javascript, y de paso aprendiste sobre JSON (pronunciado yeison), que es un formato que nos permite transmitir datos entre computadoras, el cual se extrajo de los tipos de datos de javascript, así que cuando alguien te diga que un programa genera/recibe/produce datos en JSON, ya sabes a que se refieren, y ahora podes leerlo y escribirlo.

También deberías entender y poder escribir los datos iniciales en el capitulo anterior.

Creemos en la Web: Un poco de lógica a la vista

Hasta el momento las paginas que creamos carecen de interactividad, el contenido se muestra, pero no responde a ninguna acción de nuestra parte.

También sucede que son estáticas, todo el contenido de la pagina esta en el HTML, no hay forma de usar el mismo HTML para mostrar información que varíe en el tiempo o según contexto.

Para agregar dinamicidad y que la pagina muestre contenido distinto según el contexto vamos a hacer uso de una herramienta llamada plantillas (template en ingles) o también vistas (view en ingles).

Estas plantillas nos permiten describir el HTML con "huecos" indicando donde van los datos que necesitamos, pero los datos provienen de otro lugar, la plantilla toma los datos y los reemplaza en los "huecos".

Esto nos permite también ahorrarnos trabajo cuando tenemos que mostrar muchos datos que tienen la misma estructura, definimos la plantilla para un elemento y le indicamos a la plantilla que lo muestre tantas veces como elementos haya en una lista.

Para agregar interactividad, es decir, que la pagina reaccione a acciones del usuario, vamos a usar un nuevo lenguaje llamado javascript, que nos permite indicar rutinas que modifican los datos en respuesta a acciones iniciadas por el usuario. Nuestras plantillas son notificadas de los cambios en los datos y actualizan su contenido.

Contemos

El primer ejemplo va a empezar simple, nuestro dato va a ser un numero, que indica cuantas veces se apretó un botón, es decir, un contador.

Cuando el usuario hace click en el botón, incrementamos en 1 el contador.

Empecemos con el HTML que ya conocemos:

<div>
    <p>Contador: <span>0</span></p>
    <button>Incrementar</button>
</div>

Que resulta en:

Contador: 0

Lo único nuevo es el tag button que no habíamos visto hasta ahora porque no sirve de mucho si no sabemos como hacerlo interactivo.

Muy linda aplicación, pero notaras que si hacemos click en el botón no pasa nada...

Es porque no le indicamos dos cosas:

  • Que sucede cuando se hace click en el botón
  • De donde saca el valor del contador y donde lo muestra

Para lo primero necesitamos indicarle al botón "cuando esto sucede, hace esto otro", Para lo segundo necesitamos indicarle:

  • El estado inicial de nuestros datos
    • En este caso, un contador inicialmente en cero
  • Donde mostrar ese contador en nuestro HTML

Para eso vamos a usar un proyecto llamado vuejs, que nos permite hacer esto y mucho mas.

Primero tenemos que incluir vuejs en nuestra pagina, así su funcionalidad esta disponible, esto lo hacemos con un tag script dentro del head de nuestra pagina:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>

Lo segundo que necesitamos hacer, es indicarle a vuejs, que parte de nuestro HTML es su responsabilidad, ya que podemos tener distintas partes de la pagina manejadas por distintas aplicaciones. Esto se lo indicamos agregando un identificador al tag raíz de nuestra aplicación e indicando ese identificador cuando inicializamos la aplicación.

<div id="mi-app-1">
    <!-- nuestra aplicación va acá -->
</div>

Luego necesitamos inicializar nuestra app, para esto le indicamos cual es su estado inicial y cual es el id de la raíz del HTML de nuestra app.

Esta parte va a requerir bastante explicación ya que vamos a usar un nuevo lenguaje que quizás hayas oído mencionar: javascript.

<script>
  new Vue({
    el: '#mi-app-1',
    data: {count: 0}
  });
</script>

El tag script te parecerá conocido ya que lo usamos para incluir aplicaciones de otros, como vuejs o a-frame en capítulos anteriores. Ahora vamos a escribir nuestros propios programas. Empezando con uno corto.

Al incluir vue.min.js lo que hicimos fue cargar un archivo con código javascript adentro, que lo que hace es registrar un identificador llamado Vue en nuestra pagina.

Este identificador es lo que se llama un constructor, un constructor es una función especial que al invocarla con la instrucción new nos devuelve un nuevo objecto. No te preocupes, son muchos conceptos que vamos a ir explorando en breve, pero por ahora sabe que para crear una nueva app usando vuejs, tenemos que crear un nuevo objeto de tipo Vue, el cual esta disponible porque incluimos el script vue.min.js.

El constructor Vue necesita información para crear una aplicación, mínimamente necesita saber:

  • Cual es el id del tag raíz donde la app va a correr

En nuestro caso el id es mi-app-1, como hay muchas formas de indicar el tag raíz, para que vue sepa que es un id le ponemos # al principio.

  • Cual es el estado inicial de nuestra aplicación

En nuestro caso un solo campo, llamado count inicializado a 0.

No te preocupes por ahora con los detalles de javascript, para las siguientes apps vas a poder copiar el código y solo cambiar el id y los datos iniciales.

Ok, inicializamos nuestra app, pero el HTML esta vacío, como le decimos "mostrá el valor de count acá"?

Como habrás visto hasta ahora, cuando un programador necesita decirle algo a una computadora, en lugar de usar un formato existente, inventa un lenguaje nuevo, hasta ahora aprendimos HTML, CSS y javascript, todos con formatos distintos, con nuestro lenguaje de plantillas no iba a ser la excepción, pero por suerte es bastante simple.

También vale la pena aclarar que con estos 4 lenguajes (HTML, CSS, javascript y un lenguaje de plantillas) es suficiente para hacer cualquier tipo de aplicación como las que usas día a día en internet.

Para indicar los "huecos" donde van los datos, usamos el siguiente formato:

<div id="mi-app-1">
    <p>Contador: <span>{{count}}</span></p>
    <button>Incrementar</button>
</div>

Como veras dentro del tag span escribimos {{count}} lo que significa "pone el valor del campo count acá".

El resultado es:

Contador: {{count}}

Un avance, pero el botón aun no hace nada...

Lo que le queremos indicar es "cuando el usuario haga click, hace esto", lo cual se logra agregando un atributo especial al botón, especial porque lo entiende vuejs, ese atributo se llama @click:

<div id="mi-app-1">
    <p>Contador: <span>{{count}}</span></p>
    <button @click="count = count + 1">Incrementar</button>
</div>

Contador: {{count}}

La magia esta acá:

<button @click="count = count + 1">Incrementar</button>

Le decimos "cuando el usuario haga click en este botón, corre el código count = count + 1, es decir, el nuevo valor de count es igual al viejo valor de count mas 1.

Probalo haciendo click, debería incrementarse.

Una Lista (de tareas)

Hasta ahora mostramos un valor y agregamos interactividad a nuestra pagina, pero de la introducción aun falta una cosa: evitar repetición.

Eso vamos a ver ahora haciendo una aplicación para listar tareas.

Como siempre necesitamos tener un tag raíz para nuestra aplicación, un estado inicial, mostrar los datos y agregar interactividad.

Nuestro estado inicial va a ser una lista con 0 o mas datos de tipo texto indicando la tarea a realizar, empecemos con un par de tareas iniciales así podemos practicar repetición antes de agregar interactividad:

<script>
  new Vue({
    el: '#todo-app',
    data: {
        tareas: [
            'Conquistar el mundo',
            'Abolir el patriarcado',
            'Comprar pan'
        ]
    }
  })
</script>

Nuestro estado inicial contiene un campo llamado tareas que es una lista de tres valores, los tres son de tipo texto (otro tipo que vimos es el tipo numérico para el contador)

Ahora con nuestra lista de tareas inicializada, podemos mostrarla en la pantalla, si fuéramos a hacerlo a la vieja usanza, haríamos algo así:

<ul>
    <li>Conquistar el mundo</li>
    <li>Abolir el patriarcado</li>
    <li>Comprar pan</li>
</ul>

Que se vería así:

  • Conquistar el mundo
  • Abolir el patriarcado
  • Comprar pan

Pero obviamente esto no nos sirve, queremos listar las tareas de nuestra lista de datos, con lo que aprendimos hasta ahora podríamos intentar:

<ul>
    <li>{{tarea}}</li>
</ul>

Pero esto no funciona porque no tenemos una sola tarea, sino muchas, y el identificador tarea no esta definido, tenemos el identificador tareas, sin embargo, estamos bastante cerca..., para repetir un fragmento de HTML cuyo contenido se encuentra en una lista tenemos que indicarle a vue algo así como "para cada tarea en la lista de tareas, mostrá este HTML", veamos como se hace:

<div id="todo-app">
    <ul>
        <li v-for="tarea in tareas">{{tarea}}</li>
    </ul>
</div>

Lo que resulta en:

  • {{tarea}}

Si entendés un poco de ingles veras que nuestra idea "para cada tarea en la lista de tareas, mostrá este HTML" se traduce bastante similar.

Usamos el atributo v-for (la v es de vue, v-for es un atributo que vue entiende, como @click antes), dentro del valor del atributo le decimos, "tarea in tareas", lo cual completo v-for="tarea in tareas" leído de corrido casi se lee "for tarea in tareas" que se traduce "para tarea en tareas".

El tag con el atributo v-for y sus tags hijos se van a repetir tantas veces como elementos haya en la lista tareas, en nuestro caso 3 veces.

Agregando nuevas tareas

Como agregamos nuevas tareas a nuestra lista? para eso vamos a necesitar un lugar donde podamos escribir la descripción de la nueva tarea y un botón para agregar la tarea a la lista.

El campo de texto lo creamos con el tag input, el botón como ya vimos antes, con el tag button, probemos un poco el HTML:

<input>
<button>Crear nueva tarea</button>

Muy bonito pero eso no hace nada, como "conecto" el contenido del tag input a un campo en mis datos?

primero necesitamos crear un nuevo campo en nuestros datos iniciales para el contenido de la tarea a agregar, luego necesitamos indicarle al tag input que su contenido es el valor del campo, llamemoslo tituloNuevo:

<div id="todo-app">
  <input v-model="tituloNuevo">
  <button @click="tareas.push(tituloNuevo); tituloNuevo = '';">Crear nueva tarea</button>

  <ul>
    <li v-for="tarea in tareas">{{tarea}}</li>
  </ul>
</div>
<script>
  new Vue({
    el: '#todo-app',
    data: {
        tituloNuevo: '',
        tareas: [
            'Conquistar el mundo',
            'Abolir el patriarcado',
            'Comprar pan'
        ]
    }
  })
</script>

Probemos:

  • {{tarea}}

Vamos por partes:

<input v-model="tituloNuevo">

Agregamos el atributo v-model para indicarle a vue "el contenido de este tag esta conectado al valor de tituloNuevo en nuestros datos.

<button @click="tareas.push(tituloNuevo); tituloNuevo = '';">Crear nueva tarea</button>

Nuestro viejo amigo @click en el botón hace dos cosas, primero agrega un elemento al final de la lista tareas, usando el método push, que agrega un elemento al final de la lista que esta antes del punto, el elemento a agregar se lo indicamos entre paréntesis, en este caso queremos agregar el valor contenido en tituloNuevo.

Pero eso no es todo, también queremos limpiar el contenido del tag input así el usuario puede escribir una tarea nueva sin tener que borrar el contenido que ya se agrego a la lista.

Para eso necesitamos indicar una instrucción nueva, y como ya tenemos una, necesitamos separarla, en javascript las instrucciones se separan con punto y coma.

La segunda instrucción actualiza el valor de tituloNuevo a el texto vacío, indicado con dos comillas juntas ''.

Borrando tareas

Agregar tareas sin poderlas eliminar no suena a una buena experiencia de usuario, necesitamos poder borrar tareas, para eso al lado de cada tarea vamos a agregar un botón que al ser clickeado va a borrar dicha tarea.

Pero para poder borrar la tarea necesitamos saber su posición en la lista para poder decir "borra el elemento numero 2 de la lista tareas", para eso vamos a hacer uso de una variación del atributo v-for que nos permite obtener la posición (indice) del valor que estamos mostrando, el formato es:

<li v-for="(tarea, pos) in tareas">
    <span>{{pos}}: {{tarea}}</span>
    <button @click="$delete(tareas, pos)" style="float: right">X</button>
</li>

Vamos por partes, primero usamos el formato (tarea, pos) in tareas para que vue nos devuelva no solo cada elemento en la lista sino si posición (indice), el cual vamos a nombrar pos.

<li v-for="(tarea, pos) in tareas">

Luego para ver que estamos haciendo las cosas bien, mostramos el valor de pos antes de la descripción de cada tarea:

<span>{{pos}}: {{tarea}}</span>

Finalmente agregamos un botón, que al ser clickeado llama a la función $delete de vuejs, que recibe dos parámetros, el primero es la lista a la que le queremos remover un elemento, el segundo parámetro es la posición o indice que queremos remover.

<button @click="$delete(tareas, pos)" style="float: right">X</button>

El resultado con un poco mas de formato:

{{pos}} {{tarea}}

Código completo:

<!doctype html>
<html>
  <head>
        <meta charset="utf-8">
        <title>Vue: Lista de Tareas</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
  </head>
  <body class="m-3">
        <div id="todo-app">
          <input v-model="tituloNuevo" type="text" class="form-control" id="tareaDesc" placeholder="Descripción de tarea">

          <div class="my-2 text-center">
                <button @click="tareas.push(tituloNuevo); tituloNuevo = '';"
                                class="btn btn-primary">Crear nueva tarea</button>
          </div>

          <table class="table table-bordered table-striped table-hover">
                <tbody>
                  <tr v-for="(tarea, pos) in tareas">
                        <td>{{pos}}</td>
                        <td>{{tarea}}</td>
                        <td class="text-center">
                          <button @click="$delete(tareas, pos)" class="btn btn-danger btn-sm">X</button>
                        </td>
                  </tr>
                </tbody>
          </table>
        </div>
        <script>
          new Vue({
                el: '#todo-app',
                data: {
                  tituloNuevo: '',
                  tareas: [
                        'Conquistar el mundo',
                        'Abolir el patriarcado',
                        'Comprar pan'
                  ]
                }
          })
        </script>
  </body>
</html>

Riak Core Tutorial Part 6: Handoff

The content of this chapter is in the 04-handoff branch.

https://gitlab.com/marianoguerra/tanodb/tree/04-handoff

How it Works

With quorum requests we are halfway in our way to tolerate failures in cluster nodes, our values are written to more than one vnode but if a node dies and another takes his work or if we add a new node and the vnodes must be rebalanced we need to handle handoff.

The reasons to start a handoff are:

  • A ring update event for a ring that all other nodes have already seen.
  • A secondary vnode is idle for a period of time and the primary, original owner of the partition is up again.

When this happens riak_core will inform the vnode that handoff is starting, calling handoff_starting, if it returns false it's cancelled, if it returns true it calls is_empty, that must return false to inform that the vnode has something to handoff (it's not empty) or true to inform that the vnode is empty, in our case we ask for the first element of the ets table and if it's the special value '$end_of_table' we know it's empty, if it returns true the handoff is considered finished, if false then a call is done to handle_handoff_command passing as first parameter an opaque structure that contains two fields we are insterested in, foldfun and acc0, they can be unpacked with a macro like this:

handle_handoff_command(?FOLD_REQ{foldfun=Fun, acc0=Acc0}, _Sender, State) ->

The FOLD_REQ macro is defined in the riak_core_vnode.hrl header file which we include.

This function must iterate through all the keys it stores and for each of them call foldfun with the key as first argument, the value as second argument and the latest acc0 value as third.

The result of the function call is the new Acc0 you must pass to the next call to foldfun, the last Acc0 must be returned by the handle_handoff_command.

For each call to Fun(Key, Entry, AccIn0) riak_core will send it to the new vnode, to do that it must encode the data before sending, it does this by calling encode_handoff_item(Key, Value), where you must encode the data before sending it.

When the value is received by the new vnode it must decode it and do something with it, this is done by the function handle_handoff_data, where we decode the received data and do the appropriate thing with it.

When we sent all the key/values handoff_finished will be called and then delete so we cleanup the data on the old vnode .

You can decide to handle other commands sent to the vnode while the handoff is running, you can choose to do one of the followings:

  • Handle it in the current vnode
  • Forward it to the vnode we are handing off
  • Drop it

What to do depends on the design of you app, all of them have tradeoffs.

The signature of all the responses is:

-callback handle_handoff_command(Request::term(), Sender::sender(), ModState::term()) ->
{reply, Reply::term(), NewModState::term()} |
{noreply, NewModState::term()} |
{async, Work::function(), From::sender(), NewModState::term()} |
{forward, NewModState::term()} |
{drop, NewModState::term()} |
{stop, Reason::term(), NewModState::term()}.

A diagram of the flow is as follows:

+-----------+      +----------+        +----------+
|           | true |          | false  |          |
| Starting  +------> is_empty +--------> fold_req |
|           |      |          |        |          |
+-----+-----+      +----+-----+        +----+-----+
      |                 |                   |
      | false           | true              | ok
      |                 |                   |
+-----v-----+           |              +----v-----+     +--------+
|           |           |              |          |     |        |
| Cancelled |           +--------------> finished +-----> delete |
|           |                          |          |     |        |
+-----------+                          +----------+     +--------+

Implementing it

We need to add logic to all the empty callbacks related to handoff:

handle_handoff_command(?FOLD_REQ{foldfun=FoldFun, acc0=Acc0}, _Sender,
                       State=#state{partition=Partition, kv_state=KvState}) ->
    lager:info("fold req ~p", [Partition]),
    KvFoldFun = fun ({Key, Val}, AccIn) ->
                        lager:info("fold fun ~p: ~p", [Key, Val]),
                        FoldFun(Key, Val, AccIn)
                end,
    {AccFinal, KvState1} = tanodb_kv_ets:foldl(KvFoldFun, Acc0, KvState),
    {reply, AccFinal, State#state{kv_state=KvState1}};

handle_handoff_command(Message, _Sender, State) ->
    lager:warning("handoff command ~p, ignoring", [Message]),
    {noreply, State}.

handoff_starting(TargetNode, State=#state{partition=Partition}) ->
    lager:info("handoff starting ~p: ~p", [Partition, TargetNode]),
    {true, State}.

handoff_cancelled(State=#state{partition=Partition}) ->
    lager:info("handoff cancelled ~p", [Partition]),
    {ok, State}.

handoff_finished(TargetNode, State=#state{partition=Partition}) ->
    lager:info("handoff finished ~p: ~p", [Partition, TargetNode]),
    {ok, State}.

handle_handoff_data(BinData, State=#state{kv_state=KvState}) ->
    TermData = binary_to_term(BinData),
    lager:info("handoff data received ~p", [TermData]),
    {{Bucket, Key}, Value} = TermData,
    {ok, KvState1} = tanodb_kv_ets:put(KvState, Bucket, Key, Value),
    {reply, ok, State#state{kv_state=KvState1}}.

encode_handoff_item(Key, Value) ->
    term_to_binary({Key, Value}).

is_empty(State=#state{kv_state=KvState, partition=Partition}) ->
    {IsEmpty, KvState1} = tanodb_kv_ets:is_empty(KvState),
    lager:info("is_empty ~p: ~p", [Partition, IsEmpty]),
    {IsEmpty, State#state{kv_state=KvState1}}.

delete(State=#state{kv_state=KvState, partition=Partition}) ->
    lager:info("delete ~p", [Partition]),
    {ok, KvState1} = tanodb_kv_ets:dispose(KvState),
    {ok, KvState2} = tanodb_kv_ets:delete(KvState1),
    {ok, State#state{kv_state=KvState2}}.

Trying it

To test it we will first start a devrel node, put some values and then join two other nodes and see on the console the handoff happening.

To make sure the nodes don't know about each other in case you played with clustering already we will start by removing the devrel builds:

rm -rf _build/dev*

And build the nodes again:

make devrel

Now we will start the first node and connect to its console:

make dev1-console

We generate a list of some numbers:

(tanodb1@127.0.0.1)1> Nums = lists:seq(1, 10).

[1,2,3,4,5,6,7,8,9,10]

And with it create some bucket names:

(tanodb1@127.0.0.1)2>
Buckets = lists:map(fun (N) ->
    list_to_binary("bucket-" ++ integer_to_list(N))
end, Nums).

[<<"bucket-1">>,<<"bucket-2">>,<<"bucket-3">>,
 <<"bucket-4">>,<<"bucket-5">>,<<"bucket-6">>,<<"bucket-7">>,
 <<"bucket-8">>,<<"bucket-9">>,<<"bucket-10">>]

And some key names:

(tanodb1@127.0.0.1)3>
Keys = lists:map(fun (N) ->
    list_to_binary("key-" ++ integer_to_list(N))
end, Nums).

[<<"key-1">>,<<"key-2">>,<<"key-3">>,<<"key-4">>,
 <<"key-5">>,<<"key-6">>,<<"key-7">>,<<"key-8">>,<<"key-9">>,
 <<"key-10">>]

We create a function to generate a value from a bucket and a key:

(tanodb1@127.0.0.1)4>
GenValue = fun (Bucket, Key) -> [{bucket, Bucket}, {key, Key}] end.

And then put some values to the buckets and keys we created:

(tanodb1@127.0.0.1)5>
lists:foreach(fun (Bucket) ->
    lists:foreach(fun (Key) ->
        Val = GenValue(Bucket, Key),
        tanodb:put(Bucket, Key, Val)
    end, Keys)
end, Buckets).

ok

Now that we have some data let's start the other two nodes:

make dev2-console

In yet another shell:

make dev3-console

This part should remind you of the first chapter:

make devrel-join
Success: staged join request for 'tanodb2@127.0.0.1' to 'tanodb1@127.0.0.1'
Success: staged join request for 'tanodb3@127.0.0.1' to 'tanodb1@127.0.0.1'
make devrel-cluster-plan
=============================== Staged Changes =========================
Action         Details(s)
------------------------------------------------------------------------
join           'tanodb2@127.0.0.1'
join           'tanodb3@127.0.0.1'
------------------------------------------------------------------------


NOTE: Applying these changes will result in 1 cluster transition

########################################################################
                         After cluster transition 1/1
########################################################################

================================= Membership ===========================
Status     Ring    Pending    Node
------------------------------------------------------------------------
valid     100.0%     34.4%    'tanodb1@127.0.0.1'
valid       0.0%     32.8%    'tanodb2@127.0.0.1'
valid       0.0%     32.8%    'tanodb3@127.0.0.1'
------------------------------------------------------------------------
Valid:3 / Leaving:0 / Exiting:0 / Joining:0 / Down:0

WARNING: Not all replicas will be on distinct nodes

Transfers resulting from cluster changes: 42
  21 transfers from 'tanodb1@127.0.0.1' to 'tanodb3@127.0.0.1'
  21 transfers from 'tanodb1@127.0.0.1' to 'tanodb2@127.0.0.1'
make devrel-cluster-commit
Cluster changes committed

On the consoles from the nodes you should see some logs like the following, I will just paste some as example.

On the sending side:

00:17:24.240 [info] Starting ownership transfer of tanodb_vnode from
'tanodb1@127.0.0.1' 1118962191081472546749696200048404186924073353216 to
'tanodb2@127.0.0.1' 1118962191081472546749696200048404186924073353216

00:17:24.240 [info] fold req 1118962191081472546749696200048404186924073353216
00:17:24.240 [info] fold fun {<<"bucket-1">>,<<"key-1">>}:
    [{bucket,<<"bucket-1">>},{key,<<"key-1">>}]

...

00:17:24.241 [info] fold fun {<<"bucket-7">>,<<"key-8">>}:
    [{bucket,<<"bucket-7">>},{key,<<"key-8">>}]

00:17:24.281 [info] ownership transfer of tanodb_vnode from
'tanodb1@127.0.0.1' 1118962191081472546749696200048404186924073353216 to
'tanodb2@127.0.0.1' 1118962191081472546749696200048404186924073353216
    completed: sent 575.00 B bytes in 7 of 7 objects in 0.04 seconds
    (13.67 KB/second)

00:17:24.280 [info] handoff finished
    1141798154164767904846628775559596109106197299200:
    {1141798154164767904846628775559596109106197299200,
        'tanodb3@127.0.0.1'}

00:17:24.285 [info] delete
    1141798154164767904846628775559596109106197299200

On the receiving side:

00:13:59.641 [info] handoff starting
    1050454301831586472458898473514828420377701515264:
    {hinted,{1050454301831586472458898473514828420377701515264,
        'tanodb1@127.0.0.1'}}

00:13:59.641 [info] is_empty
    182687704666362864775460604089535377456991567872: true

00:14:34.259 [info] Receiving handoff data for partition
    tanodb_vnode:68507889249886074290797726533575766546371837952 from
    {"127.0.0.1",47440}

00:14:34.296 [info] handoff data received
    {{<<"bucket-8">>,<<"key-1">>},
        [{bucket,<<"bucket-8">>},{key,<<"key-1">>}]}

...

00:14:34.297 [info] handoff data received
    {{<<"bucket-3">>,<<"key-7">>},
        [{bucket,<<"bucket-3">>},{key,<<"key-7">>}]}

00:14:34.298 [info] Handoff receiver for partition
    68507889249886074290797726533575766546371837952 exited after
    processing 5 objects from {"127.0.0.1",47440}

Riak Core Tutorial Part 5: Quorum Requests

The content of this chapter is in the 03-quorum branch.

https://gitlab.com/marianoguerra/tanodb/tree/03-quorum

How it Works

Quorum requests allows sending a command to more than one vnode and wait until a number of responses are received before considering the request succesful.

To implement this we need a process that distributed the requests to the first N vnodes in the preference list and waits for at least W response to arrive before returning to the requester.

We use a gen_fsm to implement this process, which looks like this:

+------+    +---------+    +---------+    +---------+              +------+
|      |    |         |    |         |    |         |remaining = 0 |      |
| Init +--->| Prepare +--->| Execute +--->| Waiting +------------->| Stop |
|      |    |         |    |         |    |         |              |      |
+------+    +---------+    +---------+    +-------+-+              +------+
                                              ^   | |
                                              |   | |        +---------+
                                              +---+ +------->|         |
                                                             | Timeout |
                                      remaining > 0  timeout |         |
                                                             +---------+

Implementing it

To implement it we need to change the code in tanodb.erl instantiate a FSM to handle the request instead of sending the command directly to one vnode.

get(Bucket, Key, Opts) ->
    K = {Bucket, Key},
    Params = K,
    run_quorum(get, K, Params, Opts).

put(Bucket, Key, Value, Opts) ->
    K = {Bucket, Key},
    Params = {Bucket, Key, Value},
    run_quorum(put, K, Params, Opts).

delete(Bucket, Key, Opts) ->
    K = {Bucket, Key},
    Params = K,
    run_quorum(delete, K, Params, Opts).

We are going to generalize that logic in a function called run_quorum, where we can pass options for N, W and Timeout to play with different values:

run_quorum(Action, K, Params, Opts) ->
    N = maps:get(n, Opts, ?N),
    W = maps:get(w, Opts, ?W),
    Timeout = maps:get(timeout, Opts, ?TIMEOUT),
    ReqId = make_ref(),
    tanodb_write_fsm:run(Action, K, Params, N, W, self(), ReqId),
    wait_for_reqid(ReqId, Timeout).

wait_for_reqid(ReqId, Timeout) ->
    receive
        {ReqId, Val} -> Val
    after
        Timeout -> {error, timeout}
    end.

To wait for the right answer we need to generate a unique identifier for each request and send it with the request itself. The identifier will come back in the message sent by the FSM once the request finishes.

If too much time passed waiting for the response we consider it an error and return before receiving it.

wait_for_reqid(ReqId, Timeout) ->
        receive
                {ReqId, {error, Reason}} -> {error, Reason};
                {ReqId, Val} -> Val
        after
                Timeout -> {error, timeout}
        end.

There are two new files:

tanodb_write_fsm.erl
The FSM logic
tanodb_write_fsm_sup.erl
The supervisor for the FSMs

Finally we need to add tanodb_write_fsm_sup to our top level supervisor in tanodb_sup.

Trying it

To test it we are going to run some calls to the API and observe that now the response contains more than one response:

(tanodb@127.0.0.1)1> B1 = b1.
(tanodb@127.0.0.1)2> K1 = k1.
(tanodb@127.0.0.1)3> V1 = v1.

First let's try to get a key that doesn't exist:

(tanodb@127.0.0.1)4> tanodb:get(B1, K1).
{ok,[{[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}},

         {[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}},

         {[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}}]}

Let's do the same call but passing options, we want to run the command in 5 vnodes and wait for the response of the 5, the request should finish under a second:

(tanodb@127.0.0.1)5> tanodb:get(k1, v1, #{n => 5, w => 5, timeout => 1000}).
{ok,[{[456719261665907161938651510223838443642478919680,
           'tanodb@127.0.0.1'],
          {not_found,{k1,v1}}},

         {[433883298582611803841718934712646521460354973696,
           'tanodb@127.0.0.1'],
          {not_found,{k1,v1}}},

         {[411047335499316445744786359201454599278231027712,
           'tanodb@127.0.0.1'],
          {not_found,{k1,v1}}},

         {[388211372416021087647853783690262677096107081728,
           'tanodb@127.0.0.1'],
          {not_found,{k1,v1}}},

         {[365375409332725729550921208179070754913983135744,
           'tanodb@127.0.0.1'],
          {not_found,{k1,v1}}}]}

Let's try deleting a key that doesn't exist:

(tanodb@127.0.0.1)6> tanodb:delete(B1, K1).
{ok,[{[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          ok},

         {[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          ok},

         {[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          ok}]}

Let's put a value:

(tanodb@127.0.0.1)7> tanodb:put(B1, K1, V1).
{ok,[{[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          ok},

         {[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          ok},

         {[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          ok}]}

Now let's get the value:

(tanodb@127.0.0.1)8> tanodb:get(B1, K1).
{ok,[{[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          {found,{{b1,k1},v1}}},

         {[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          {found,{{b1,k1},v1}}},

         {[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          {found,{{b1,k1},v1}}}]}

Let's delete it:

(tanodb@127.0.0.1)9> tanodb:delete(B1, K1).
{ok,[{[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          ok},

         {[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          ok},

         {[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          ok}]}

And try to get it back:

(tanodb@127.0.0.1)10> tanodb:get(B1, K1).
{ok,[{[1073290264914881830555831049026020342559825461248,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}},

         {[1096126227998177188652763624537212264741949407232,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}},

         {[1050454301831586472458898473514828420377701515264,
           'tanodb@127.0.0.1'],
          {not_found,{b1,k1}}}]}

Riak Core Tutorial Part 4: First Commands

The content of this chapter is in the 02-commands branch.

https://gitlab.com/marianoguerra/tanodb/tree/02-commands

This is part of a series, see the previous one at Riak Core Tutorial Part 3: Ping Command

Implementing Get, Put and Delete

For our first commands we will copy the general structure of the ping command.

We will start by adding three new functions to the tanodb.erl file:

get(Bucket, Key) ->
    ReqId = make_ref(),
    send_to_one(Bucket, Key, {get, ReqId, {Bucket, Key}}).

put(Bucket, Key, Value) ->
    ReqId = make_ref(),
    send_to_one(Bucket, Key, {put, ReqId, {Bucket, Key, Value}}).

delete(Bucket, Key) ->
    ReqId = make_ref(),
    send_to_one(Bucket, Key, {delete, ReqId, {Bucket, Key}}).

And generalizing the code used by ping to send a command to one vnode:

send_to_one(Bucket, Key, Cmd) ->
    DocIdx = riak_core_util:chash_key({Bucket, 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).

In tanodb_vnode.erl we will need to first create an instance of the key-value store per vnode at initialization and keep a reference to its state in the vnode state record:

-record(state, {partition, kv_state}).

init([Partition]) ->
    {ok, KvState} = tanodb_kv_ets:new(#{partition => Partition}),
    {ok, #state { partition=Partition, kv_state=KvState }}.

We then need to add three new clauses to the handle_command callback to handle our two new commands, which translate almost directly to calls in the kv module:

handle_command({put, ReqId, {Bucket, Key, Value}}, _Sender,
               State=#state{kv_state=KvState, partition=Partition}) ->
    Location = [Partition, node()],
    {Res, KvState1} = tanodb_kv_ets:put(KvState, Bucket, Key, Value),
    {reply, {ReqId, {Location, Res}}, State#state{kv_state=KvState1}};

handle_command({get, ReqId, {Bucket, Key}}, _Sender,
               State=#state{kv_state=KvState, partition=Partition}) ->
    Location = [Partition, node()],
    {Res, KvState1} = tanodb_kv_ets:get(KvState, Bucket, Key),
    {reply, {ReqId, {Location, Res}}, State#state{kv_state=KvState1}};

handle_command({delete, ReqId, {Bucket, Key}}, _Sender,
               State=#state{kv_state=KvState, partition=Partition}) ->
    Location = [Partition, node()],
    {Res, KvState1} = tanodb_kv_ets:delete(KvState, Bucket, Key),
    {reply, {ReqId, {Location, Res}}, State#state{kv_state=KvState1}};

Trying it

First let's try to get a key that doesn't exist:

(tanodb@127.0.0.1)1> B1 = b1.
(tanodb@127.0.0.1)2> K1 = k1.
(tanodb@127.0.0.1)3> V1 = v1.
(tanodb@127.0.0.1)4> tanodb:get(B1, K1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  {not_found,{b1,k1}}}}

The structure of the response is:

{UniqueRequestReference, {[PartitionId, NodeId], CommandResponse}}.

Let's try deleting a key that doesn't exist:

(tanodb@127.0.0.1)5> tanodb:delete(B1, K1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  ok}}

Let's put a value:

(tanodb@127.0.0.1)6> tanodb:put(B1, K1, V1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  ok}}

Now let's get the value:

(tanodb@127.0.0.1)7> tanodb:get(B1, K1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  {found,{{b1,k1},v1}}}}

Let's delete it:

(tanodb@127.0.0.1)8> tanodb:delete(B1, K1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  ok}}

And try to get it back:

(tanodb@127.0.0.1)9> tanodb:get(B1, K1).
{Ref, {[1050454301831586472458898473514828420377701515264,
        'tanodb@127.0.0.1'],
  {not_found,{b1,k1}}}}