Lets' Talk
As is common in many environments, a lot of my projects consist of more than 1 application, usually multiple Phoenix applications.
Sometimes it is necessary for one application to notify the other(s) that something has happened, eg a record changed.
To facilitate this, Elixir offers us a few nice libraries built upon the Erlang networking core.
The most basic example, as seen in the Elixir guides, is connecting 2 nodes and running some code on the other node.
But that is not what we are talking about here. What we want is a simple mechanism to have the nodes subscribe to a topic and listen for notifications.
Sounds familiar? Yes I am talking about PubSub or Publish and Subscribe, offered to us by Phoenix.PubSub
A small example, open iex:
Mix.install([{:phoenix_pubsub, "~> 2.1"}])
This will install, Phoenix.PubSub
in our current iex session.
Phoenix.PubSub.Supervisor.start_link(name: :my_pubsub)
Phoenix.PubSub.subscribe(:my_pubsub, "test")
Start the PubSub supervisor, and give it a name, next subscribe to a topic.
Phoenix.PubSub.broadcast(:my_pubsub, "test", %{a: 1})
Now broadcast something on this pubsub and topic, after flushing we can see the message
we received.
flush
%{a: 1}
:ok
So we can now publish and subscribe within this session, not too useful... yet.
Make it a project
mix new talkies --sup
cd talkies
Edit mix.exs
and add Phoenix.PubSub
to it.
...
defp deps do
[
{:phoenix_pubsub, "~> 2.1"}
]
end
...
And start it with the app in application.ex
...
def start(_type, _args) do
children = [
{Phoenix.PubSub, name: :my_pubsub}
]
...
Get the dependencies and start the app.
mix deps.get
iex -S mix
If you repeat the subscribe
and broadcast
, you will see this already works.
Make 2 nodes talk
So now that we have this little app
, let's use it to make 2 versions of our app talk to each other.
In 2 seperate terminals start the app with a short name, iex --sname app1 -S mix
and iex --sname app2 -S mix
.
You will notice the iex
prompt now includes the node name, app1@computername
.
We will do our subscribe in app1
, do a publish in app2
, flush
in app1
, and... nothing happened ???
We have started the apps with networking enabled, but we haven't yet connect them.
In either app, do Node.connect(:"app2@computername")
, where you use the name of the other node as seen in the iex prompt, this name is given as an atom
.
Now do the broadcast again and flush, yes, we have received our message. Great!
So what have we learned so far?
-
If you start your app with the
--sname
, you can connect to other nodes on the same computer. -
If you start a
Phoenix.PubSub
in multiple nodes, with the same name, they automically are connected. - We have not seen this in our example, but all nodes need to use the same erlang cookie! See the above mentioned guide for more info.
Cluster the apps
While using Node.connect/1
is fun in an example, in real life it is not that useful.
There is however an excellent library that easily allows us to connect our apps, libcluster
.
Add {:libcluster, "~> 3.3}
to your dependencies and mix deps.get
.
Open application.ex
and add the following child, {Cluster.Supervisor, [topologies, [name: Talkies.ClusterSupervisor]]}
.
libcluster
uses what they call a topology
to specify how you want your nodes to connect, this can be with a fixed host list and epmd
, or something like kubernetes, rancher, etc.
We will use the Cluster.Strategy.Gossip
strategy, this requires almost no configuration on our part and works nicely on the same host, or even within the same network.
Add the topology to the start
method before the children
;
topologies = [
talkies: [
strategy: Cluster.Strategy.Gossip,
secret: "my_secret_conversations"
]
]
Only nodes knowing the secret
can talk to each other. Start both apps again.
After starting the second app, you should notice a message in your console saying 10:19:18.074 [info] [libcluster:talkies] connected to :app1@computer
.
Our nodes have found each other and connected, you can see this with Node.list/0
.
Se if we subscribe and broadcast, this will already work, nice.
In a Phoenix
app you would do the same, add the dependency, your topology and add the Cluster.Supervisor
to the list of children.
Once you do this and you start 2 instances of your app (using --sname
) channels, ... will work across the instances.
You can subscribe to broadcasts in your live view pages, or use a GenServer
, ...
defmodule Talkies.SubscriberServer do
use GenServer
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE_)
end
def init(_) do
Phoenix.PubSub.subscribe(Talkies.PubSub, "notices")
{:ok, %{}}
end
def handle_info(message, state) do
# Do something with the message
{:noreply, state}
end
end
Going into production
We had some fun playing with our nodes in development, but at a certain moment of time we need to deploy to production.
I deploy all my apps on my own servers using LXC containers, so I am going to show you how I do this here.
First we are going to move our topology to our config files and get it in start
with topologies = Application.get_env(:talkies, :topology)
.
config/dev.exs
config :talkies,
topology: [
talkies: [
strategy: Cluster.Strategy.Gossip,
secret: "my_secret_conversations"
]
]
config/test.exs
config :talkies, topology: []
config/runtime.exs
topology_hosts =
System.get_env("PARTNERS") ||
raise """
environment variable PARTNERS is missing, no cluster will be active
"""
config :talkies,
topology: [
talkies: [
strategy: Cluster.Strategy.Epmd,
config: [
hosts: topology_hosts |> String.split(",") |> Enum.map(&String.to_atom/1)
]
]
]
To make it easy to add aditional nodes I opted to put them in an environment variable, seperated by commas.
In my systemd unit file, I already had the ERLANG_COOKIE
set, but I ensure it is the same for all apps that want to talk to each other.
I then add the PARTNERS
:
Environment=ERLANG_COOKIE=A_very_super_secret_cookie_that_noone_can_guess
Environment=PARTNERS=app2@example.org,app3@example.org
Note that we use long names in production and that epmd
should be reachable on all hosts.
On the topic of long names, something to consider, I was deploying 2 new apps a couple of days back in the same container and ran into a snag.
I first set RELEASE_DISTRIBUTION
to sname
, to use short names, same host, why need long names, api@mdx-staging
, but that didn't work.
The erlang networking refused to find the others. So I set it back to name
, and tried api@localhost
, this failed too as localhost
is not a valid long name, duh!
Setting it to the full resolvable hostname would not work because this resolved to an IPv6 address and epmd while listening on ::4369
refused to connect.
So I resolved to point localhost.example.org
to 127.0.0.1
in my /etc/hosts
file and then set PARTNERS=api@localhost.example.org
, that did work!
So don't forget to set the PARTNERS
environment variable to include all other nodes in each app, do not inlude itself.
When we start the apps now, you should see nodes connect in the logs, if you see messages saying it was not able to connect, check your PARTNERS
and epmd
.
This is the most challening part.
Conclusion
Aside from the epmd
troubles, it is quite easy to add inter-app communication in Elixir.
But why should you, would you?
I mostly use it to inform one of the other apps something has changed, say you have a customer dashboard and a back-office dashboard in 2 seperate apps.
When a customer places an order, you can broadcast it and do something useful in the back-office app, process the order, send mail, sms, ...
Or in one of my projects, I have one app that talks to a number of Nerves
devices over Phoenix
channels, the other app has a dashboard that displays realtime information from the devices, and can send back commands.
Don't forget if you are using multiple different Phoenix
applications to have them all use the same name for the PubSub
, by default it will be YourApp.PubSub
!
Tip! Although epmd
will not allow communication without the correct ERLANG_COOKIE
it's better to restrict access to all but the PARTNERS
.