Why it's not supported to call repositories by PID?

I’m playing around with programming against an interface in Elixir. I’d like to have a Storage protocol, not Repo so it won’t clash with Ecto’s naming, and have at least one implementation backed by a repository.

defprotocol Whatever.Storage do
  def all(storage, schema)
  # and so on
end

defmodule Whatever.Storage.Ecto do
  # The PID of the repository
  defstruct [:pid]

  defimpl Whatever.Storage do
    def all(storage, schema) do
      Ecto.Repo.all(storage.pid, schema)
    end
  end
end

I’ll create the Whatever.Storage.Ecto struct while initializing the main supervisor and pass it down the “interested” processes. Unfortunately, Ecto doesn’t provide a way to call a repository by PID. You have to set it as a dynamic repository first, which modifies the process dictionary. I don’t like this and would like to have an alternative.

Is there such alternative?

1 Like

There is not. Dynamic repositories already are the feature that was added for supporting dynamic repo processes.

2 Likes

Working with pids directly instead of names is often a bit of a code smell in any application, as pids are best used as private information by supervising/orchestrating processes to manage the topology of your distribution transparently for you. Ecto managing connection pools being an adjacent example: if you had direct handles to connection pids yourself, it would be hard for it to do that with the desired guarantees.

In this case, Ecto does not expose a pid-based API because Repos are backed by an involved supervision tree to give you a lot of functionality, and it wants to push you towards using names so it can do that unhindered, restart your repo as a new pid if it fails, etc. It also uses a named singleton pattern for Repo supervisors as modules so it can do a lot of functionality in your repo module at compile time.

For this situation, I would recommend that you change your ecto storage struct wrapper to defstruct [:repo, :dynamic], and accept a repo module and optional dynamic repo atom/pid. Then to play nicely with dynamic or non repos, something like this should work:

  defimpl Whatever.Storage do
    def all(storage, schema) do
      old_dynamic = storage.repo.get_dynamic_repo()
      if storage.dynamic do
        storage.repo.put_dynamic_repo(storage.dynamic)
      end
      try do
        storage.repo.all(schema)
      after
        storage.repo.put_dynamic_repo(old_dynamic)
      end
    end
  end

This can probably cleaned up a little and extracted into a macro if you want to reduce boilerplate.

Aside:

Outside of ecto, if you are trying to make a struct that models a reference to a process, I would recommend having it be of type GenServer.server() rather than a pid, that is, a server name that fits into a standard supervision tree/start_link situation and can passed into GenServer.whereis/1 for resolution to a pid/node. This supports named processes and via tuples as well as pids, and in many common cases you won’t even need to resolve to a pid/node as many functions accept this signature. It allows you to play nicely with GenServer, Registry, DynamicSupervisor, PartitionSupervisor, and many libraries for cross-node process registration.

5 Likes

Thank you, I’ll make use of the aside for sure.

In my opinion the named singleton pattern is a code smell because the named process becomes a hidden dependency everywhere it’s used. It leads to resorting to process ownership for tests and what not. If your process depends on a repository then just use a :rest_for_one supervision strategy and order your processes as appropriate. After all, this is what supervision is for - defining process’ lifecycle. I think we can make use of more good programming practices and not resort to BEAM being amazing so much.

3 Likes