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:
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 pasterminate/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'unGenServer
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
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.
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.
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)