# Roulette
Scalable PubSub client library which uses HashRing-ed gnatsd-cluster
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `roulette` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:roulette, "~> 1.0.4"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/roulette](https://hexdocs.pm/roulette).
## Prepare your own PubSub module
```elixir
defmodule MyApp.PubSub do
use Roulette, otp_app: :my_app
end
```
## Configuration
Setup configuration like following
```elixir
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
```elixir
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
end
```
## Simple Usage
```elixir
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
end
```
Anywhere else you want to publish message in your app.
```elixir
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](https://github.com/nats-io/gnatsd#full-mesh-required)
![roulette_01](https://user-images.githubusercontent.com/30877/41829326-0c040e5a-7875-11e8-8680-d89bf8ccbd39.png)
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_02](https://user-images.githubusercontent.com/30877/41829331-0e27822a-7875-11e8-8407-fce8268e06ac.png)
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`.
![roulette_03](https://user-images.githubusercontent.com/30877/41829333-0f67267c-7875-11e8-994a-745fec2ebdd6.png)
## Full Configuration Description
Here is a minimum configuration example,
You must setup `servers` list.
Put your load-balancers' hostname into it.
```elixir
config :my_app, MyApp.PubSub,
servers: [
"gnatsd-cluster1.example.org",
"gnatsd-cluster2.example.org"
]
```
Or else, you can use keyword list for each host.
```elixir
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 `Publisher` and `Subscriber` connections
- :subscriber - setup `Subscriber` connections only
- :publisher - setup `Publisher` connections 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.
```elixir
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
```elixir
topic = "foobar"
MyApp.PubSub.pub!(topic, data)
```
### Subscribe
ok/error style.
`sub/1` returns Supervisor.on_start()
```elixir
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
```elixir
MyApp.PubSub.sub!(topic)
```
### Unsubscribe
ok/error style.
`sub/1` returns Supervisor.on_start()
```elixir
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
```elixir
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.
```elixir
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
end
```
## LICENSE
MIT-LICENSE
## Author
Lyo Kaot <lyo.kato __at__ gmail.com>