web-dev-qa-db-fra.com

Arrêt progressif de GenServer

J'écris une application Elixir avec GenServer qui démarre une application externe au démarrage et la ferme et effectue d'autres nettoyages à la sortie. J'ai ajouté une fonctionnalité de démarrage dans init/1 code de rappel et de nettoyage dans terminate/2 rappel.

Le code init fonctionne correctement lorsque le GenServer est démarré, et la méthode terminate est également appelée lorsque le :stop le signal est envoyé manuellement, mais dans les cas d'arrêts et d'interruptions inattendus (comme dans le cas de la frappe Ctrl + C) dans IEx, le code de fin n'est pas appelé.


Actuellement, j'ai parcouru des tonnes de fils de discussion, d'articles de blog et de documentation, notamment:

De Elixir Docs - GenServers :

Si le GenServer reçoit un signal de sortie (ce n'est pas :normal) de tout processus quand il n'intercepte pas les sorties, il se fermera brusquement pour la même raison et donc n'appellera pas terminate/2. Notez qu'un processus ne [~ # ~] pas [~ # ~] intercepte les sorties par défaut et un signal de sortie est envoyé quand un processus lié se termine ou que son nœud est déconnecté.

Par conséquent, il n'est pas garanti que terminate/2 est appelé lorsqu'un GenServer se ferme. Pour de telles raisons, nous recommandons généralement que des règles de nettoyage importantes se produisent dans des processus séparés, soit en utilisant la surveillance, soit par des liens eux-mêmes.

mais je ne sais absolument pas comment obtenir :init.stop, linked processes ou n'importe quoi d'autre pour travailler avec ça (puisque c'est ma première fois avec GenServers).


Voici mon code:

defmodule MyAwesomeApp do
  use GenServer

  def start do
    GenServer.start_link(__MODULE__, nil)
  end

  def init(state) do
    # Do Bootup stuff

    IO.puts "Starting: #{inspect(state)}"
    {:ok, state}
  end

  def terminate(reason, state) do
    # Do Shutdown Stuff

    IO.puts "Going Down: #{inspect(state)}"
    :normal
  end
end

MyAwesomeApp.start
19
Sheharyar

Pour augmenter les chances d'appeler le rappel terminate, le processus serveur doit intercepter les sorties. Cependant, même avec cela, le rappel peut ne pas être invoqué dans certaines situations (par exemple, lorsque le processus est brutalement tué ou lorsqu'il se bloque). Pour plus de détails, voir ici .

Comme mentionné, si vous souhaitez arrêter poliment votre système, vous devez invoquer :init.stop, qui fermera récursivement l'arborescence de supervision provoquant l'appel de rappels terminate.

Comme vous l'avez remarqué, il n'y a aucun moyen de capturer de façon brutale les sorties de processus BEAM OS. C'est une propriété auto-définie: le processus BEAM se termine soudainement, il ne peut donc pas exécuter de code (puisqu'il s'est terminé) ????. Par conséquent, si BEAM se termine brutalement, le rappel ne sera pas appelé.

Si vous souhaitez inconditionnellement faire quelque chose lorsque BEAM meurt, vous devez le détecter à partir d'un autre processus du système d'exploitation. Je ne sais pas quel est votre cas d'utilisation exact, mais en supposant que vous ayez des besoins importants pour cela, alors exécuter un autre nœud BEAM, sur la même (ou une autre) machine, pourrait fonctionner ici. Ensuite, vous pouvez avoir un processus sur un nœud surveillant un autre processus sur un autre nœud, de sorte que vous pouvez réagir même si BEAM est brutalement tué.

Cependant, votre vie sera plus simple si vous n'avez pas besoin d'exécuter inconditionnellement une logique de nettoyage, alors demandez-vous si le code dans terminate est un must, ou plutôt un Nice-to-have.

10
sasajuric

Je peux vous proposer deux solutions.

Le premier est mentionné dans les documents.

Notez qu'un processus ne piège PAS les sorties.

Vous devez faire en sorte que votre processus de serveur gen se termine. Pour faire ça:

Process.flag(:trap_exit, true)

Cela fait que votre processus appelle terminate/2 à la sortie.

Mais une autre solution, c'est de remettre cette initialisation au supérieur hiérarchique. Demandez ensuite au superviseur de transmettre la référence de l'application externe au serveur gen. Mais ici, vous n'avez pas de rappel de type terminate pour quitter l'application externe si nécessaire. L'application externe sera simplement supprimée lorsque le superviseur s'arrêtera.

5
vfsoraki

Si vous essayez de le faire fonctionner dans iex et Process.flag(:trap_exit, true) ne fonctionne pas, assurez-vous que vous utilisez GenServer.start au lieu de GenServer.start_link sinon le processus Shell se bloquera et le recouvrement n'aura pas d'importance.

Voici un exemple:

defmodule Server do

    use GenServer
    require Logger

    def start() do
      GenServer.start(__MODULE__, [], [])
    end


    def init(_) do
      Logger.info "starting"
      Process.flag(:trap_exit, true) # your trap_exit call should be here
      {:ok, :some_state}
    end

    # handle the trapped exit call
    def handle_info({:EXIT, _from, reason}, state) do
      Logger.info "exiting"
      cleanup(reason, state)
      {:stop, reason, state} # see GenServer docs for other return types
    end

    # handle termination
    def terminate(reason, state) do
      Logger.info "terminating"
      cleanup(reason, state)
      state
    end

    defp cleanup(_reason, _state) do
      # Cleanup whatever you need cleaned up
    end

end

Dans iex, vous devriez maintenant voir un appel de sortie piégé

iex> {:ok, pid} = Server.start()
iex> Process.exit(pid, :something_bad)
1
user3703967