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