Sharing single gen_server from supervisor free library in umbrela/distributed app

Hello fellow elixirers,

I need help with designing API of library:

Introduction

I have application composed of several services that are for performance reason grouped in several umbrella projects. This application is storing in memory a lot of data, it needs to do that because of operations done on them. To solve this problem, there is shared library that is added as dependency to every service, let’s call that library commonlib. I would like to to keep services separated, so If in the future I took service out of umbrella and I run it allone, it will only start services from commonlib that it needs.

Today solution

Actualy, I have working solution. For now I use flags in application config. Every application switch on which services it needs and then these services are started in commonlib. Piece of code responsible for returning of children that needs to by started in commonlib looks like this:

defmodule App.CommonLib.Children do
  def get_children() do
    []
    |> maybe_use(App.CommonLib.Resource1, use_resource1?())
    |> maybe_use(App.CommonLib.Resource2, use_resource2?())
    |> maybe_use(App.CommonLib.Resource3, use_resource3?())
  end

  defp maybe_use(children, module, use_cache?) do
    if to_bool(use_cache?), do: [module | children], else: children
  end

  defp use_resource1?() do
    Application.get_env(:firex, :use_resource1, false)
  end

  defp use_resource2?() do
    Application.get_env(:firex, :use_resource2, false)
  end

  defp use_resource3?() do
    Application.get_env(:firex, :use_resource3, false)
  end

  defp to_bool(value) when is_function(value), do: value.()
  defp to_bool({module, fun}), do: apply(module, fun, [])
  defp to_bool(value), do: value
end

I was also experimenting by creating DynamicSupervisor in common lib which had function start_link(unique attributes of resource) and then if someone else started resource with same attributes before, I just ignored start_link and returned pid of existing process.

Need for change

Recently I was studiing library guidelines which make my think that I can actualy left these resources as something that would by started by client application. After some experiments I found out that it works really well. It save a lot of hadache in testing and reduced need for starting dynamic supervisor, registers and so on.

Problem

This architecture poses one problem, since some services are together in umbrellas, I get a lot of {:error, {:already_started, pid}}. My first idea was that it is simple, I will just return pid to supervisor from start_link of resource and it will observe shared resource.

def start_link(_ \\ nil) do
  case GenServer.start_link(__MODULE__, :ok, name: __MODULE__) do
    {:error, {:already_started, pid}} -> {:ok, pid}
    response -> response
  end
end

But since supervisor is counting on process to by started with spawn_link, it ignored when resource got restarted or crashed.

So I start thinking about creating server that would in it’s start_link start resource, if it already exists, start observing it, and if it crash, crash and get restarted. I was kind of expecting that there should by someone that already went this way so I started to looking for library that is doing this, and only one I found is signleton. This library API looks similiarly to second API I’m using in commonlib.

So mi question is, did I hit most acceptable solution with solution 2 from commonlib. Is having single worker observed by multiple supervisor kind of bad practice? (since I don’t see a lot of libraries to do that). Is there any other recommendation how to design something like this?

I will be happy for any constructive response and I hope I explained mi situation enought,
Thank you for reading this long question <3
() - potato

Have you tried Process.link(pid) before returning {:ok, pid} ? Not sure if it opens some possibility of race conditions when more than a supervisor is linked but since you’re registering with a name it should be ok?

1 Like

After I put so much effort to compose question I found out that I missed something in process documentation :joy: , thank you for this answer, it solves a lot