Calling a GenServer on a remote machine

Hey all, happy 2024!

I think I have on my hands a configuration and OTP problem - I’m pretty sure I have all the pieces, just not sure how to put it together.

I have a Nerves device which is connected to a sensor. I have created a GenServer to read the sensor periodically, save the data and provide it upon a caller’s request. This device also a has a LiveView UI which graphs the value of the sensor over time. This all works great on the device.

Where it breaks down is in development - I want to be able to develop the UI on my local machine, but use live data by calling the GenServer running on the device. I have enabled epmd and can connect to the remote node through iex, but now I’m trying to figure out how to ‘re-route’ all of the calls to the sensor GenServer to the Nerves device when running the application on the host.

I am considering two approaches to this problem. First, GenServer will accept any name registration as outlined in the docs, including processes on remote nodes. I think I could look up the remote PID on application start, and store that in application config for use later. As a second approach, I think there should be a way to use Registry or :global to lookup the correct process to direct the requests to.

Are there any recommendations on how to best do this?

Thanks!

1 Like

Is the device you deploy Nerves on, and your workstation have similar CPU architecture, i.e. both are ARM or both are Intel CPU instruction based? I never tried if it actually is a problem, but endianness may be a problem Endianness - Wikipedia if you try talking between x86 and ARM64 nodes in the cluster.

But answering your question, you can use Horde, and in particular Horde.Registry Horde.Registry — Horde v0.8.7 to register the Pids of all the GenServers in the cluster and then use it just like normal Registry to find those Pids. The APIs are the same. I am using it in the homogenous cluster of the same machines and it works great, again, I am not sure if the endianness won’t mess it up.

This is a bold statement, do you have any sources that can confirm this?

Also curious about the endianness, as I’ve only done homogeneous clusters, but I wouldn’t have thought it would be an issue as it is being packed by BEAM.

For instance, I had to receive/decode a packet from an Erlang machine and it took me a minute to realize that the packet header (2-bytes to indicate the size of the packet) was in big-endian format.

No, and it’d be interesting to see what works and what doesn’t work.

I am mentioning it because I saw the erlang/elixir code explicitly handling endiness in some blog posts about handling data from smart sensors, and I suspect that the distribution protocol will handle it just fine but the payloads you send between nodes may require conversion. For example: Smart Sensors with Erlang and AtomVM: Smart cities, smart houses and manufacturing monitoring - Erlang Solutions

2 Likes

Well that is another question.

I am not entirely sure, but my thinking is that inter-node communication is using functionality from term_to_binary, there seems to be a more comprehensive documentation here: Erlang -- External Term Format

1 Like

Hi.
Maybe the Spawn project GitHub - eigr/spawn: Spawn - Actor Mesh (I’m a co-creator) can help you. If you need, you can open a discussion in the repository and we’ll continue there. Or on our discord channel (link in repo).

Hey thanks for the suggestion! While I am on different architectures (ARM Raspberry Pi 3 vs Intel Mac), I haven’t run into endianness as an issue yet. I do have to be careful about endianness when reading the sensors, because each one is different according to its own data sheet. However, when sending messages between BEAM nodes I haven’t had any issues. I assume that it is taken care of by term_to_binary as mentioned by @D4no0. If I run into any while testing though, I’ll be sure to update here!

Horde.Registry looks like a decent option. I do notice that it looks like I would have to structure my whole application around using their registry instead of the built-in one. I’d like to avoid that, but it would work for debugging purposes.

What I was thinking is that for Nerves application.ex, we have a pathway to configure different supervision trees based on the MIX_TARGET the code was compiled for - see below:

defmodule MyApp.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    opts = [strategy: :one_for_one, name: MyApp.Supervisor]

    children =
      [
        # children for both targets here
      ] ++ children(target())

    Supervisor.start_link(children, opts)
  end

  # List all child processes to be supervised
  def children(:host) do
    [
      # children for host

      # possible to do some sort of "remote" sensor here?
      {MyApp.Sensor.Remote, node: "nerves@nerves.local"}
    ]
  end

  def children(_target) do
    [
      # children for targets
      {MyApp.Sensor, polling_interval: 1_000}
    ]
  end

  def target() do
    Application.get_env(:my_app, :target)
  end
end

It would be nice to have that functionality available in a lightweight way - ie, only have the changes apply on the host side

Looks interesting! I’ve not heard of the project before. After a Quick Look through the docs though, it looks a little more heavy-weight than I think I need in this situation? I don’t have a DB and not using Kubernetes. Was there a specific module in the project you think would help that I can look deeper into?

I suggested it precisely because it’s got the same API as normal Registry, I also use it in production and it works fine.

Erlang -- pg is simple, flexible and already included in Erlang.

1 Like

Thanks.
When I saw the main comment what caught my attention was that you need a strong reference but following the principle of locality transparency, in this regard Spawn would help since you would just name your actors and be able to call them in a transparent way.
If you used the SDK available for Elixir you would be able to use Spawn just by configuring some environment variables without needing to use k8s. This is not how we recommend using it since you, in this case, would have to take care of the infrastructure yourself. But you might want to go along those lines.
Regarding the database, this is only mandatory for stateful actors. When using stateless actors you don’t need to connect to a database.
We could try to make a POC or something if we could get more details on its architecture. But there are some example repositories that can help you get a more general idea of the concepts.

Well, there are a lot of good libraries above, but as I see the problem statement, one does not need any external library, nor even pg here.

Just name a process as {:global, Name}, and make sure the nodes are connected (by calling Node.connect/1 and checking it with Node.list/0.

Then :global.whereis_name/1 will give you a remote PID back, and you might use all the functions from GenServer using {:global, Name} as a remote process name.

2 Likes

For all GenServer calls/casts/etc you could use a helper function via() or @via macro with Application env.
And could use {atom, node} if the server is locally registered at another node or {:global, name}