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 :)