Run code for each module implementing a Behaviour

Background

We have an app that receives requests from several sources and must redirect them to other services. In order to facilitate this, the company developed a simple App with a Plugin system using Behaviours. Basically, we have a module that receives requests and then passes the requests to the plugins that implement the module’s Behaviour.

As is normal in any plugin architecture, we don’t know how many plugins we will have and more can be created in the future.

For each plugin we would like to run some specific code, let’s say, print it’s name.

Config file

The first approach we took was to just have a config.exs with an array of plugin atoms. Then in the code, we traverse the plugins array and run the print function.

Problem

However I dislike this approach. Instead of just adding a plugin file I also have to alter the config file. I was wondering if there was a better way of doing it, that detects plugins on launch and then runs some specific code for each one.

We have a folder specifically for the plugins, so we know how many we have at compile time ( each file is a plugin ).

Question

Is there an automated way to detect how many plugins we have without manually changing the config.exs each time?

You could use Erlang’s gen_event module. When you have a request come in, you hand it off to your gen_event process, which would foreward it to all registered handlers. You can even add or remove additional handlers at run time.

So, each handler would be module implementing the Behaviour we have?
I fail to see how that differs from what we have ( we iterate through all modules implementing said behaviour and call the functions on each one ).

I believe you can do something like this:

__DIR__
|> Path.join("some_folder/**/*.ex")
|> Path.wildcard()
|> Enum.each(&Code.require_file/1)
|> Enum.map(fn {module, _} -> module end)
|> Enum.each(& &1.my_function())
1 Like

Mostly this part. And then there are the benefits of using standard OTP.

The following is taken directly from the top of the documentation.

An event manager implemented using this module has a standard set of interface functions and includes functionality for tracing and error reporting. It also fits into an OTP supervision tree.

Anyone coming to your codebase will know, or at least can look up, how exactly gen_event works. With a completely custom solution, someone will need to understand how it works, the caveats that there may be, etc.

Basically, why reinvent the wheel when people have been using and fixing issues with gen_event for years? And depending on your needs, the ability to add and remove handlers at run time is a great piece of functionality.

What about using your plugins as .exs files that follow an API? then you could maybe add and remove them at run-time without a config file? (note, I’m relatively new to Elixir and not sure how well this might work)

@Ankhers
I understand that using GenEvent may have been beneficial but this is a company project that is already done. The architecture is layed out and I can only work with what I have. Using GenEvent is out of the question.

Not only that, using GenEven would likely also require for each plugin module to be a process and we don’t want that. The entire thing runs on one process which is just fine by now, even though I agree that having each module on a separate process would help with fault tolerance.

@darkmarmot

What about using your plugins as .exs files that follow an API? then you could maybe add and remove them at run-time without a config file?

As far as I know, adding an .exs are usually used for testing and config files, not for PROD deployment. As for the API, we are already doing that via Behaviours. I suggest you have a look at Elixir Behaviours VS Protocols:

I use @on_load to register plugins:

https://hexdocs.pm/elixir/Module.html#module-on_load