Ejemplo Elixir. Gestión de login + OTP
En este ejemplo veremos un gestor de login parcialmente desarrollado.
Es un problema sencillo de gestión de estado…
El interfaz del actor estará compuesto por dos métodos
rq_key
y rq_login
Solución manual
defmodule LoginManager do def start, do: ... def (rq_key door), do: ... def (rq_login door, user_name, codded_pass), do: ... end
En Elixir podemos modelarlo…
defmodule LoginManager do def start do from_pid = self spawn(fn -> loging_manager_loop :w_rq_key, from_pid end) end defp loging_manager_loop :w_rq_key, from_pid do ... end defp loging_manager_loop :w_rq_login, key, from_pid do ... end end
La implementación del estado
:w_rq_login
…defp loging_manager_loop :w_rq_login, key, from_pid doreceive do {:rq_login, user_name, codded_pass} ->
if login_rq_info_ok { user_name, key, codded_pass } do send(from_pid, :login_ok)
else loging_manager_loop :w_rq_key, from_pid end ignoring -> IO.puts "Ignoring message #{inspect ignoring}"
loging_manager_loop :w_rq_login, key, from_pid after 5000 -> IO.puts "exiting by timeout"
end end
Punto de entrada para el estado w_rq_login | |
El mensaje esperado es rq_login con nombre de usuario y la contraseña codificada | |
Si es ok, respondemos con login_ok y terminamos | |
Si recibimos cualquier otro mensaje, lo trazamos pero no cambiamos el estado | |
Si no recibimos la petición de login, buy |
El punto
4
es importante. Si recibimos un mensaje no esperado, en caso de que no lo recojamos, se quedará indefinidamente en la cola del proceso. Si esta cola creciese demasiado… peligro
El
5
también es interesante. Si nadie nos pide el login, nos vamos liberando la memoria y recursos.
Dejando de lado los dos últimos puntos, el resultado es muy elegante.
Y para probarlo…
Test
defmodule LoginManagerTest do use ExUnit.Case test "request login" do rec = fn -> receive do msg -> msg; after 1000 -> IO.puts "time out" end end d = LoginManager.start LoginManager.rq_key d key = rec.() coded_password = to_string :erlang.crc32(to_string(:erlang.crc32("joseluis"<>"1111"))<>key) LoginManager.rq_login d, "joseluis", coded_password :login_ok = rec.() end end
Aquí tenemos otros detalles poco elegantes. La recepción de la respueta es un poco incómoda. Estamos simulando una llamada síncrona con esteroides en un sistema asíncrono.
Estos patrones y dificultades son frecuentes. Para no gestionarlos manualmente siempre, debemos utilizar
otp
. No sólo evitaremos reescribir el mismo código, además utilizaremos un código de gran calidad, estabilidad y con patrones adecuados.
código completo solución manual
defmodule LoginManager do @moduledoc """ This is a small Elixir example working with process as actor A partial implementation of a login manager. In order to login, it's necessary to call **rq_key** and compose the **rq_login** with the user name, the key and password using a non reversible (crc32) function """ def start do from_pid = self spawn(fn -> loging_manager_loop :w_rq_key, from_pid end) end def (rq_key door), do: send(door, :rq_key) def (rq_login door, user_name, codded_pass), do: send(door, {:rq_login, user_name, codded_pass}) defp loging_manager_loop :w_rq_key, from_pid do :random.seed(:erlang.now) receive do :rq_key -> key = to_string(:random.uniform 100000) send(from_pid, key) loging_manager_loop :w_rq_login, key, from_pid ignoring -> IO.puts "Ignoring message #{inspect ignoring}" loging_manager_loop :w_rq_key, from_pid after 5000 -> IO.puts "exiting by timeout defp loging_manager_loop :w_rq_key, from_pid do" end end defp loging_manager_loop :w_rq_login, key, from_pid do receive do {:rq_login, user_name, codded_pass} -> if login_rq_info_ok { user_name, key, codded_pass } do send(from_pid, :login_ok) else loging_manager_loop :w_rq_key, from_pid end ignoring -> IO.puts "Ignoring message #{inspect ignoring}" loging_manager_loop :w_rq_login, key, from_pid after 5000 -> IO.puts "exiting by timeout defp loging_manager_loop :w_rq_login, key, from_pid do" end end defp login_rq_info_ok { _user_name, key, codded_pass } do if(codded_pass == to_string :erlang.crc32(to_string(:erlang.crc32("joseluis"<>"1111"))<>key)) do :true else :false end end end
Solución OTP
Utilizando exactor. Un conjunto de macros para hacer más cómodo y legible el trabajo con
GenServer
y otros elementos de OTP
defmodule LoginManager do use ExActor.GenServer definit do :random.seed(:erlang.now) initial_state(:w_rq_key) end defcall rq_key, state: :w_rq_key do key = to_string(:random.uniform 100000) set_and_reply({:w_rq_login, key}, {:key, key}) end defcall (rq_login user_name, codded_pass), state: {:w_rq_login, key} do if login_rq_info_ok { user_name, key, codded_pass } do set_and_reply(:login_ok, :login_ok) else set_and_reply(:rq_key, :login_rejected) end end defp login_rq_info_ok { _user_name, key, codded_pass } do if(codded_pass == to_string :erlang.crc32( to_string(:erlang.crc32("joseluis"<>"1111"))<>key)) do :true else :false end end end
El código se ha contraido tanto y es tan legible, que pongo directamente el programa completo.
Y esto no es todo, el programa de test, ahora también es mucho más sencillo, elegante y claro.
defmodule LoginManager2Test do use ExUnit.Case test "login ok" do {:ok, lm} = LoginManager.start {:key, key} = LoginManager.rq_key lm codded_password = to_string :erlang.crc32(to_string(:erlang.crc32("joseluis"<>"1111"))<>key) :login_ok = LoginManager.rq_login lm, "joseluis", codded_password end test "login failed" do {:ok, lm} = LoginManager.start {:key, key} = LoginManager.rq_key lm codded_password = to_string :erlang.crc32(to_string(:erlang.crc32("joseluis"<>"2222"))<>key) :login_rejected = LoginManager.rq_login lm, "joseluis", codded_password end end
No todo es una maravilla con
OTP
. Algunos modelos sencillos manuales no son tan evidentes conOTP
. Pero esto es sólo el principio.
Comentarios