Change Application module into GenServer module?

Background

I have an application module that creates a supervision tree. So far we only ever needed 1 instance of this application (let’s call it Clint, because I like westerns) but now we need to be able to have several separate instrances of this application.

So I took the chance and I am converting this into a library with a nice interface that creates Clint applications (GenServers) and returns them for further use.

Problem

The problem is that this GenServer I want to return is not actually a GenServer, it is a Supervisor with it’s own tree:

defmodule Clint.Application do
  use Application

  def start(_type, _args) do
    Supervisor.start_link(
      [
        Clint.ProcessRegistry,
        Clint.Pool,
        Clint.Cache
      ],
      strategy: :one_for_one
    )
  end
end

Clint has a ProcessRegistry, a Pool of workers and a Cache it can use.

Questions

So I have several questions now:

  1. How do I transform this code into a GenServer that is a Supervisor at the same time?
  2. What is the difference between an Application and a GenServer?

You don’t. Start the supervisor in the places you need it to be started.

An application has not really much to do with supervision or processes. An application is what you start/stop on the beam. It can return a root supervisor, but there’s no requirement to do so.

1 Like

One of the easiest ways is to provide a start_link or child_spec function with your top level module that describes how to start all of these processes. A super naive approach would look like this:

defmodule Clint do
  def child_spec(_arg) do
    children = [
        Clint.ProcessRegistry,
        Clint.Pool,
        Clint.Cache
    ]

    %{
      id: Clint,
      type: :supervisor,
      start: {Supervisor, :start_link, [children, [strategy: :one_for_one]]}
    }
  end
end

The main piece of complexity that you’ll run into here is how to name all of your processes. From the looks of it you are probably naming each of your registry, pool, and cache by some global name. If you attempt to run multiple of these clint supervision trees then you’ll hit naming collisions. So you’ll want to pass in a name that you can address each of them by:

Clint.start_link(name: "clint1")
Clint.start_link(name: "clint2")

# and update your childspec to accept a name
defmodule Clint do
  def child_spec(args \\ []) do
    name = Keyword.get(args, :name, Clint)

    children = [
        {Clint.ProcessRegistry, [name: name]},
        {Clint.Pool, [name: name]},
        {Clint.Cache, [name: name]}
    ]

    %{
      id: name,
      type: :supervisor,
      start: {Supervisor, :start_link, [children, [strategy: :one_for_one]]}
    }
  end
end

I’m also passing the base “name” around here so you’ll want to have all of your lower processes create a name using the one that we’re providing. You’ll also need to update all of your calls to those functions to use the passed in name.

2 Likes

All of that aside depending on your use case it might just be easier to leave Clint as an application and simply have it support multiple use cases. It really depends on who needs to control what. If you’re application absolutely needs to keep track of the different “Clints” then you may want to structure things more as a “library”. If its fine to hide this complexity in the Clint application itself then its probably going to be easier to go that route.

Going the library route (which I am doing) is something we definitely need here, this is why I am having all this trouble. In reality, this is a headache I managed to postpone a few weeks ago, but since this keeps comming back to bite me, it’s time I do it right once and for all.

I may have to restructure the process registry, that is true. But I don’t see another way.

My main problem here is that I don’t know if a process can be both a GenServer and a Supervisor at the same time. This is rather confusing to me and I would appreciate some pointers.

It can, but it’s not suggested to do so. A supervisor is just a process like any other. What makes it a supervisor and not a genserver is what is run within the process. There’s also the parent library by @sasajuric, which afaik goes into using a genserver in a way it mimics what a supervisor does. Generally though it seems to be the common tone to not merge the responsibilities and if you need a genserver as well have it as another child to the supervisor.

1 Like

Really interesting pointers!

So, in my case, how would you do it?
Imagine the following scenario:

Clint.create(opts) #creates and returns a supervisor.
Clint.fire_shotgun(args) #a call with the side effect of killing people

The problem here is that Clint.create needs to do two things:

  1. Create the Clint process that is a GenServer that will do the killing
  2. Create a Supervisor to make sure our Clint GenServer, Cache and everything else stays alive in the face of other bad cowboys

How would you do this?

:wave:

I’d probably make Clint to be a child of the Clint supervisor

base_name = :clint1
{:ok, sup_pid} = Clint.Supervisor.start_link(base_name: base_name)
Clint.fire_shotgun(base_name, head: :shot, very: :nice)
1 Like

And you would use a local name. I think I like the idea !
It’s not like I’m going to dynamically generate atoms, so it shouldn’t be an issue !