Packages

Scalable PubSub client for HashRing-ed gnatsd-cluster

Current section

Files

Jump to
roulette lib roulette.ex
Raw

lib/roulette.ex

defmodule Roulette do
@moduledoc ~S"""
Scalable PubSub client library which uses HashRing-ed gnatsd-cluster
## 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)
## 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
```
"""
defmacro __using__(opts \\ []) do
quote location: :keep, bind_quoted: [opts: opts] do
@config Roulette.Config.load(__MODULE__, opts)
@spec pub(String.t, any) :: :ok | :error
def pub(topic, data) do
Roulette.Publisher.pub(__MODULE__, topic, data)
end
@spec pub!(String.t, any) :: :ok
def pub!(topic, data) do
case pub(topic, data) do
:ok -> :ok
:error -> raise Roulette.Error, "failed to pub: #{topic}"
end
end
@spec sub(String.t) :: Supervisor.on_start
def sub(topic) do
Roulette.Subscriber.sub(__MODULE__, topic)
end
@spec sub!(String.t) :: pid
def sub!(topic) do
case sub(topic) do
{:ok, pid} -> pid
other -> raise Roulette.Error, "failed to sub: #{inspect other}"
end
end
@spec unsub(String.t | pid) :: :ok | {:error, :not_found}
def unsub(topic_or_pid) do
Roulette.Subscriber.unsub(__MODULE__, topic_or_pid)
end
@spec unsub!(String.t | pid) :: :ok
def unsub!(topic_or_pid) do
case unsub(topic_or_pid) do
:ok -> :ok
other -> raise Roulette.Error, "failed to unsub: #{inspect other}"
end
end
@spec child_spec(any) :: Supervisor.child_spec
def child_spec(_opts) do
Roulette.Supervisor.child_spec(__MODULE__, @config)
end
end
end
end