Chamagne Bastien
Développeur indépendant à Pau

Découverte locale d'acteurs

Modèle Acteur Elixir

Objectif

Nous allons voir les différentes façons qu’ont les acteurs pour découvrir d’autres acteurs présents sur le même nœud.

Parent<>Enfant

On ne peut pas vraiment parler de “découverte” ici mais je le mentionne quand même car c’est le cas que nous avons rencontré jusqu’à présent. Par exemple dans cet article : Un acteur transitoire qui produit un résultat. Quand on utilise la fonction spawn (peu importe l’arité), celle-ci retourne directement le pid.

child_pid =
  spawn(fn ->
    receive do
      msg -> IO.puts("child received: #{msg}")
    end
  end)

send(child_pid, "hello")
child received: hello
"hello"

Info : Le “hello” ici est le retour de la fonction send/2. Elle retourne son 2ème paramètre.

Il est normal que le parent connaisse l’enfant mais l’inverse n’est pas vrai. Pour que l’enfant connaisse son géniteur, il suffit que celui-ci lui donne son pid via un message ou via la closure (contexte d’une fonction anonyme).

# avec une closure
parent_pid = self()

child_pid =
  spawn(fn ->
    receive do
      {:feed, food} -> send(parent_pid, "I hate #{food}!")
    end
  end)

send(child_pid, {:feed, "green beans"})
iex(8)> flush()
"I hate green beans!"
:ok
# avec un message
child_pid =
  spawn(fn ->
    receive do
      {:feed, food, parent_pid} -> send(parent_pid, "I hate #{food}!")
    end
  end)

send(child_pid, {:feed, "green beans", self()})
iex(8)> flush()
"I hate green beans!"

Info : flush/0 est une fonction disponible uniquement dans le terminal qui permet de vider et d’afficher les messages qui se trouvent dans la mailbox de l’acteur.

Named processes

Pour les acteurs permanents uniques, on peut tout simplement nommer les acteurs avec la fonction Process.register/2. Le nom doit être un atom. Un acteur peut avoir plusieurs noms. Un nom n’est par contre lié qu’à un seul acteur.

iex(4)> Process.whereis(:mon_shell)
nil

iex(5)> Process.register(self(), :mon_shell)
true

iex(6)> Process.whereis(:mon_shell)
#PID<0.105.0>

iex(7)> send(:mon_shell, :un_message)
:un_message

iex(8)> flush
:un_message
:ok

J’ai utilisé Process.whereis/1 qui retourne un pid associé à un nom pour la démo, mais, comme on peut le voir, on n’en a même pas besoin car la fonction send/2 fait déjà ce travail d’association.

En pratique on utilisera souvent la macro __MODULE__ pour nommer un acteur permanent. Cette macro retourne le nom du module.

Registres

Pour découvrir des acteurs dynamiques, il existe le module Registry. Ce module permet de créer un acteur qui va s’occuper de faire la correspondance entre une clé et un ou plusieurs pids. Les différences par rapport aux named processes sont les suivantes :

Reprenons l’exemple du compteur de cet article : Un acteur permanent qui préserve un état et imaginons qu’on a besoin d’un compteur pour compter le nombre de vues de pages.

defmodule Counter do
  use Agent

  # j'ai ajouté un argument optionnel
  # pour pouvoir passer un nom en paramètre
  # C'est l'équivalent de Counter.start/0 et Process.register/2 en suivant
  def start(opts \\ []) do
    Agent.start(fn -> 0 end, opts)
  end

  def stop(pid) do
    Agent.stop(pid)
  end

  def count(pid) do
    {:ok, Agent.get(pid, fn count -> count end)}
  end

  def incr(pid) do
    Agent.update(pid, fn count -> count + 1 end)
    count(pid)
  end
end
iex(2)> {:ok, _} = Registry.start_link(keys: :unique, name: PageCounters)
{:ok, #PID<0.111.0>}

iex(3)> name = {:via, Registry, {PageCounters, "My socks collection"}}
{:via, Registry, {PageCounters, "My socks collection"}}

iex(4)> {:ok, _} = Counter.start(name: name)
{:ok, #PID<0.113.0>}

iex(5)> Counter.count(name)
{:ok, 0}

iex(6)> Counter.incr(name)
{:ok, 1}

Convention : Le tuple {:via, _, _} n’est pas sorti de mon chapeau. C’est une convention qui permet de résoudre directement les pid sans avoir à faire de Registry.lookup/2

N’importe quel autre acteur peut accéder à ce compteur à partir du moment où il connaît le nom du registre : PageCounters et le nom de la page : "My socks collection". Il est aussi possible de lister les acteurs d’un même registre via Registry.select/2. Mais attention, la syntaxe fait saigner des yeux :

iex(8)> Registry.select(PageCounters, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}])
[
  {"I found a way to solve P=NP", #PID<0.114.0>, nil},
  {"My socks collection", #PID<0.113.0>, nil}
]

Conclusion

Grâce à l’utilisation de ces deux techniques, on devrait moins voir de pid se balader pour nos acteurs permanents. Les deux techniques sont complètement intégrées dans la librairie standard et toutes les fonctions d’envoi de messages (il y en a un paquet) acceptent donc des pids, des names, ou des :via tuple.

Dans le prochain article on va commencer à aborder la distribution. Il serait dommage de se restreindre à un seul nœud n’est-ce pas ?