How do I supervise an erlport GenServer?

So I have this erlport connection module:

defmodule PheedThePi.PythonConnection do
  @moduledoc """
  Module responsible for the Python connection and IO.
  """

  def start() do
    path = [
      :code.priv_dir(:pheed_the_pi), "python"
    ] |> Path.join() |> IO.inspect(label: "Python priv path")

    {:ok, pid} = :python.start([{:python_path, to_charlist(path)}])

    pid
  end

  def call(pid, module, function, arguments \\ []), do:
    :python.call(pid, module, function, arguments)

  def cast(pid, message), do:
    :python.cast(pid, message)

  def stop(pid), do:
    :python.stop(pid)

end

And my GenServer for the connection:

defmodule PheedThePi.PythonServer do
  @moduledoc """
  GenServer which manages the Python erlang port.
  """

  use GenServer
  alias PheedThePi.PythonConnection, as: Python

  def start_link(_), do:
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)

  def init(_) do
    # Get the pid of the python session.
    python_session = Python.start()
    # Start the connection to the python session.
    Python.call(python_session, :api, :register_handler, [self()])
    {:ok, python_session}
  end

  # Call a specific function given an atom as the function name
  @spec cast_function(atom(), list(any())) :: :ok
  def cast_function(function, arguments), do:
    GenServer.cast(__MODULE__, {function, arguments})

  # Send a general message to Python
  @spec send_message(any) :: :ok
  def send_message(message), do:
    GenServer.cast(__MODULE__, {:python_message, message})

  def handle_cast({:python_message, message}, python_session) do
    Python.cast(python_session, message)
    {:noreply, python_session}
  end

  def handle_cast({function, arguments}, python_session) do
    Python.call(python_session, :api, function, arguments)
    {:noreply, python_session}
  end

  def handle_info({:python, message}, python_session) do
    IO.write "Got message from Python: #{message}\n"
    {:noreply, python_session}
  end

  def terminate(_reason, python_session) do
    Python.stop(python_session)
  end

end

And my Python code:

from erlport.erlang import set_message_handler, cast
from erlport.erlterms import Atom

message_handler = None #reference to the elixir process to send result to

def message(message):
    message = message.decode("utf-8") 
    print('Hey, python has gotten the message: ' + message)
    return 'test'

def cast_message(pid, message):
    cast(pid, (Atom(b'python'), message))

def register_handler(pid):
    #save message handler pid
    global message_handler
    message_handler = pid

def handle_message(message):
    message = message.decode("utf-8")
    print("Received message from Elixir: " + message) 
    cast_message(message_handler, 'Here you go: ' + message)


set_message_handler(handle_message)

Now this works when you run expected code:

PheedThePi.PythonServer.send_message "this is a message"

:ok
iex(12)> Received message from Elixir: this is a message
Got message from Python: Here you go: this is a message

But if I were to do something stupid like:

iex(12)> PheedThePi.PythonServer.send_message 1+1                
:ok
iex(13)> [error] GenServer #PID<0.420.0> terminating
** (stop) {:message_handler_error, {:python, :"builtins.AttributeError", '\'int\' object has no attribute \'decode\'', ['  File "/home/zastrix/Documents/Personal/Phoenix/pheed_the_pi/_build/dev/lib/pheed_the_pi/priv/python/api.py", line 20, in handle_message\n    message = message.decode("utf-8")\n', '  File "/home/zastrix/Documents/Personal/Phoenix/pheed_the_pi/_build/dev/lib/erlport/priv/python3/erlport/erlang.py", line 233, in _call_with_error_handler\n    function(*args)\n']}}
Last message: {#Port<0.10>, {:data, <<131, 104, 2, 100, 0, 1, 101, 104, 4, 100, 0, 6, 112, 121, 116, 104, 111, 110, 100, 0, 23, 98, 117, 105, 108, 116, 105, 110, 115, 46, 65, 116, 116, 114, 105, 98, 117, 116, 101, 69, 114, 114, 111, 114, 107, 0, ...>>}}
State: {:state, :infinity, 0, #Port<0.10>, [], []}

I get an error message, and my erlport process dies, not my GenServer so it doesn’t restart. I couldn’t find a way but is it possible to perhaps add a Supervisor to my GenServer which supercises the erlport process?

1 Like

Why don’t You check here for valid input? If You expect binaries, but not 1 + 1, You could use a guard…

when is_binary(message)

…using any here, means anything would be ok, even 1 + 1

That was just an example of how to generate an error. In this case it was 1+1 but it will kill the Erlport GenServer if there is an exception on the Python side of code as well. It would crash it if I didn’t utf-8 decode the message argument. (Again, the code would crash if any random exception happened within Python)

I just want to know that if it is possible to supervise an Erlport, how do I do that. I’d like to supervise it from my GenServer.

As it is just a process, You could monitor it, catch DOWN message… then take appropriate mesure.

BTW there is also a :python.start_link() so it is easy to link it to another process.

1 Like

Thanks but I found an relatively easy solution. I just linked my GenServer to the erlport pid and now when it crashes, it crashes my supervised genserver. Here is the new code:

defmodule PheedThePi.PythonServer do
  @moduledoc """
  GenServer which manages the Python erlang port.
  """

  use GenServer
  alias PheedThePi.PythonConnection, as: Python

  def start_link(_), do:
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)

  def init(_) do
    # Get the pid of the python session.
    python_session = Python.start()

    # Link the python pid to the GenServer
    Process.link(python_session)

    # Start the connection to the python session.
    Python.call(python_session, :api, :register_handler, [self()])
    {:ok, python_session}
  end

  # Cast a specific function given an atom as the function name
  @spec cast_function(atom(), list(any())) :: :ok
  def cast_function(function, arguments), do:
    GenServer.cast(__MODULE__, {function, arguments})

  # Call a specific function given an atom as the function name
  @spec call_function(atom(), list(any())) :: binary()
  def call_function(function, arguments), do:
    GenServer.call(__MODULE__, {function, arguments})

  # Send a general message to Python
  @spec send_message(any) :: :ok
  def send_message(message), do:
    GenServer.cast(__MODULE__, {:python_message, message})

  def handle_cast({:python_message, message}, python_session) do
    Python.cast(python_session, message)
    {:noreply, python_session}
  end

  def handle_cast({function, arguments}, python_session) do
    Python.call(python_session, :api, function, arguments)
    {:noreply, python_session}
  end

  def handle_info({:python, message}, python_session) do
    IO.write "Got message from Python: #{message}\n"
    {:noreply, python_session}
  end

  def handle_call({:compress_img, image}, _caller, python_session) do
    bytes = Python.call(python_session, :api, :compress_img, image)
    {:reply, bytes, python_session}
  end

  def terminate(reason, python_session) do
    IO.inspect(reason, label: "Why has it died")
    Python.stop(python_session)
  end

end

Nice You find a solution.

I would trap exit from this GenServer, and restart the python session upon receiving a DOWN message… avoiding the GenSever restart, but it’s just cosmetic.

BTW You could use the following line, as it is atomic.

python_session = Python.start_link()

instead of…

# Get the pid of the python session.
python_session = Python.start()

# Link the python pid to the GenServer
Process.link(python_session)
2 Likes

Thanks. I didn’t know that the difference between start and start_link is that start_link basically links the process to the caller (I mean, now it makes sense when I wrote it). I’ll give you a solution as it’s basically what I’ve done but better ahahah!

1 Like