Roulette
Scalable PubSub client library which uses HashRing-ed gnatsd-cluster
Installation
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.
Prepare your own PubSub module
defmodule MyApp.PubSub do
use Roulette, otp_app: :my_app
endConfiguration
Setup 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]
]
# ...Application
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
endSimple Usage
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
:ok
end
endAnywhere else you want to publish message in your app.
MyApp.PubSub.pub!("foobar", data)Premised gnatsd Network Architecture
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.
Full Configuration Description
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 |
role
-
:both (default) - setup both
PublisherandSubscriberconnections -
:subscriber - setup
Subscriberconnections only -
:publisher - setup
Publisherconnections only
subscription_restart
:temporary
subscription-process sends EXIT message to consumer process when gnatsd-connection is disconnected.
:permanent
subscription-process try to keep subscription. when gnatsd-connection is disconnected, retry to sends SUBSCRIBE message through other connections.
Detailed Usage
Publish
ok/error style.
topic = "foobar"
case MyApp.PubSub.pub(topic, data) do
:ok -> :ok
:error -> :error
end
If you don't mind error handling(not recommended on production),
you can use pub!/2 instead
topic = "foobar"
MyApp.PubSub.pub!(topic, data)Subscribe
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
end
If you don't mind error handling(not recommended on production),
you can use sub!/1 instead
MyApp.PubSub.sub!(topic)Unsubscribe
ok/error style.
sub/1 returns Supervisor.on_start()
topic = "foobar"
case MyApp.PubSub.unsub("foobar") do
:ok -> :ok
{:error, :not_found} -> :ok
end
If 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
endLICENSE
MIT-LICENSE
Author
Lyo Kaot <lyo.kato at gmail.com>