Scalable PubSub client library which uses HashRing-ed gnatsd-cluster
If available in Hex, the package can be installed
by adding roulette to your list of dependencies in mix.exs:
def deps do
[
{:roulette, "~> 1.0.4"}
]
endDocumentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/roulette.
defmodule MyApp.PubSub do
use Roulette, otp_app: :my_app
endSetup configuration like following
config :my_app, MyApp.PubSub,
servers: [
[host: "gnatsd1.example.org", port: 4222],
[host: "gnatsd2.example.org", port: 4222],
[host: "gnatsd3.example.org", port: 4222]
]
# ...Append your PubSub module onto your application's supervisor
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{MyApp.Pubsub, []}
# ... other children
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor..start_link(children, opts)
end
enddefmodule MyApp.Session do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
username = Keyword.fetch!(opts, :username)
MyApp.PubSub.sub!(username)
{:ok, %{username: username}}
end
def handle_info({:pubsub_message, topic, msg, pid}, state) do
# handle msg
{:noreply, state}
end
def terminate(reason, state) do
:ok
end
endAnywhere else you want to publish message in your app.
MyApp.PubSub.pub!("foobar", data)gnatsd supports cluster-mode. This works with full-mesh and one-hop messaging system to sync events.
gnatsd's full-mesh architecture
Roulette assumes that you put a load-balancer like AWS-NBL in front of each gnatsd-clusters.
Roulette doesn't have a responsiblity for health-check and load-balancing between gnatsd-servers exists in a single gnatsd-cluster. Roulette assumes that It's a load-balancers' responsibility.
Roulette connects to each gnatsd-server through load-balancers, and doesn't mind which endpoint it connects to.
However if your application servers send PUBLISH so much,
it'll cause troubles eventuallly.
Roulette resolves this problem with Consistent Hashing.
Setup multiple gnatsd-cluster beforehand, and when your app sends
PUBLISH or SUBSCRIBE message,
"Which cluster your app sends message to" is decided by the topic.
Here is a minimum configuration example,
You must setup servers list.
Put your load-balancers' hostname into it.
config :my_app, MyApp.PubSub,
servers: [
"gnatsd-cluster1.example.org",
"gnatsd-cluster2.example.org"
]Or else, you can use keyword list for each host.
config :my_app, MyApp.PubSub,
servers: [
[host: "gnatsd-cluster1.example.org", port: 4222],
[host: "gnatsd-cluster2.example.org", port: 4222]
]If there is no port setting, 4222 is set by defaut.
| key | default | description |
|---|---|---|
| role | :both | You can choose :subscriber, :publisher, or :both |
| servers | required | servers list used as hash-ring |
| pool_size | 5 | number of connections for each gnatsd-cluster |
| ping_interval | 5_000 | sends PING message to gnatsd with this interval (milliseconds) |
| max_ping_failure | 2 | if PONG doesn't return while this number of PING sends, Roulette disconnects the connection. |
| max_retry | 10 | When it fails to send PUBLISH or SUBSCRIBE messages, it automatically retries until count of failure reaches to this number |
| max_backoff | 5_000 | max duration(milliseconds) used to calculate backoff period |
| base_backoff | 10 | base number used to calculate backoff period |
| show_debug_log | false | if this is true, Roulette dumps many debug logs. |
| subscription_restart | :temporary | You can choose :temporary or :permanent |
- :both (default) - setup both
PublisherandSubscriberconnections - :subscriber - setup
Subscriberconnections only - :publisher - setup
Publisherconnections only
subscription-process sends EXIT message to consumer process when gnatsd-connection is disconnected.
subscription-process try to keep subscription. when gnatsd-connection is disconnected, retry to sends SUBSCRIBE message through other connections.
ok/error style.
topic = "foobar"
case MyApp.PubSub.pub(topic, data) do
:ok -> :ok
:error -> :error
endIf you don't mind error handling(not recommended on production),
you can use pub!/2 instead
topic = "foobar"
MyApp.PubSub.pub!(topic, data)ok/error style.
sub/1 returns Supervisor.on_start()
topic = "foobar"
case MyApp.PubSub.sub("foobar") do
{:ok, _pid} -> :ok
other ->
Logger.warn "failed to sub: #{inspect other}"
:error
endIf you don't mind error handling(not recommended on production),
you can use sub!/1 instead
MyApp.PubSub.sub!(topic)ok/error style.
sub/1 returns Supervisor.on_start()
topic = "foobar"
case MyApp.PubSub.unsub("foobar") do
:ok -> :ok
{:error, :not_found} -> :ok
endIf you don't mind error handling(not recommended on production),
you can use unsub!/1 instead
MyApp.PubSub.unsub!(topic)In following example, you don't need to call unsub/1 on terminate/2.
Because unsub is automatically handled, the process which calls sub terminates.
defmodule MyApp.Session do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
username = Keyword.fetch!(opts, :username)
MyApp.PubSub.sub!(username)
{:ok, %{username: username}}
end
def handle_info({:pubsub_message, topic, msg, pid}, state) do
# handle msg
{:noreply, state}
end
def terminate(reason, state) do
# You don't need this line
# MyApp.PubSub.unsub(state.username)
:ok
end
endMIT-LICENSE
Lyo Kaot <lyo.kato at gmail.com>