Code Code Ship logo
 

HighlanderPG: There can be only one

April 11, 2024

In the film Highlander (1986), immortal swordsmen duel to become the last man standing. The film's tagline, "There can be only one" was the inspiration the name of my initial library, Highlander, which can be used to make a process unique within your Elixir cluster. Today I am releasing a sister package, HighlanderPG, which improves on the original in several key ways.

A regular application supervisor might look like this:

# lib/application.ex
children = [
  # Runs on every node in the cluster
  {MyProcess, init_arg}
]

Supervisor.init(children, strategy: :one_for_one)

And with HighlanderPG configured to run the process on one node only, it would look like this:

# lib/application.ex
child_spec = {MyProcess, init_arg}

children = [
  # Runs on just one node in the cluster
  {HighlanderPG, [child: child_spec, connect_opts: connect_opts()]}
]

Supervisor.init(children, strategy: :one_for_one)

Since I released Highlander 4 years ago, it has accumulated 750k downloads. The original Highlander worked really well for a lot of people, but it had some drawbacks that I set out to remedy with HighlanderPG. Some of these drawbacks were due to my decision to base it on Erlang's :global registry module, and others were down to the design decision to delegate child process supervision to a child supervisor:

  • Highlander could run your process twice with certain networking failures.
  • Highlander is simply unusable if Erlang clustering is unavailable (eg, Heroku).
  • Highlander could run your process twice if you misconfigured Erlang clustering or had any other issues.
  • Highlander implements supervision in a barebones way that is functional but leaves room for improvement.

These are all issues that are solved in the new HighlanderPG library. HighlanderPG is based on Postgres advisory locks, which means it is usable for nearly everyone. It also works on the fail-safe principle, with all failure paths lead to the process not being started. Finally, supervision has been improved. Read on for details.

Postgres Advisory Locks

HighlanderPG makes use of Postgres' advisory locks. An advisory lock, unlike a table lock or row lock, is not coupled to a specific table or row in the database. Instead, it exposes two keyspaces, consisting of either one 64-bit integer, or two 32-bit integers (these keyspaces are separate from each other, so 0::64 does not conflict with 0::32, 0::32). This allows us to build application locking logic on top of Postgres without requiring the use of a migration. And indeed, HighlanderPG does not require any migrations in order to function.

HighlanderPG's operation is simple. On boot, it will establish a connection to Postgres. It will then request an advisory lock for its child process. It then simply waits until it acquires the lock before proceeding to start its child process. If no lock is acquired, no process is started. Finally, when it is time to shut down, it first shuts down the child process, and then its connection with Postgres, freeing the lock for the next node.

Fail Safety

One drawback to the original Highlander's implementation is that if there was a configuration error, or a networking failure, you could end up with multiple instances of your child process running simultaneously. In other words, it did not fail to safety.

HighlanderPG does fail to safety. If for any reason HighlanderPG can not connect to the database, whether you misconfigured your database connection details, the network is down, or your database is down, the child process is not started. This reduces the harm caused by potential failures in the system and makes the overall system more predictable.

Supervision

HighlanderPG also improves on Highlander's supervision, now supporting proper start-up and termination of your child process, including respecting shutdown timeouts. I drew on my knowledge of Erlang's supervisor module to do this, built up largely through my work on Horde.

Now, if you specify a shutdown timeout in your child_spec, HighlanderPG will respect that.

child_spec = %{start: {m, f, a}, id: :my_process, shutdown: 10_000}

children = [
  {HighlanderPG, [child: child_spec, connect_opts: connect_opts()]}
]

Not free

The topic of sustainability of open source has become current again the past few weeks with the discovery of a backdoor in xz utils. I have contributed small patches to many open source libraries, including Erlang, and Elixir, LiveView, Ecto, Bandit, and OpenTelemetry, and released a few open source libraries of my own, which I maintain, including Horde and Highlander. I will continue to contribute to open source, and these libraries will always remain open source. But HighlanderPG is not open source.

I love open source, and I would love to contribute full time. But the simple fact is that it doesn't pay my bills. And that's why I have made the decision to start charging money for some of the software that I produce.

The price of HighlanderPG will be $100 per year. Anyone who sits down and does the math should easily conclude that it is worth the money. You can be done in 5 minutes, or you can take a week to implement the same thing, but probably not as well. On top of that, I will take care of the maintenance into the future, collecting crash reports and feedback from other users, and using that to improve your own experience with the library.

Building sustainable projects

My hope is not only that I can make some money with this project, and continue to contribute to my and others open source and closed source projects, but also that this project will inspire a few people to look closely at their own projects and examine whether they are sustainable. I think it's better for everyone if we find ways to monetize, and therefore support, our work. A year ago, Parker Selbert made a great list of open source libraries that he thought could be supported by selling closed-source add-ons, and I want to draw attention to that list again. He specifically mentions PromEx, Bandit, Credo, and Req, but I think this logic could be applied to many other libraries out there.

In closing, please subscribe to my library if you need it for your application. And if you are also an open-source maintainer, think about how you could monetize your own projects.

By Derek Kraan
Code Code Ship helps you sell your library on a subscription basis.
Read more
Share
Code Code Ship logo

Get in touch

You can get in touch with us via email or on our Discord server. Click the button for an invite.

Subscribe

Always stay up to date by subscribing to our newsletter.