View Source GenServer Capabilities
Slipstream's callbacks are effectively a superset of GenServer
, and in
fact each module-based Slipstream socket client is a GenServer
. The
GenServer
abstraction provides a simple but powerful interface for message
sending and internal state in a system with many actors.
Tutorial
In this tutorial, we'll cover the basics of GenServer operations with
Slipstream clients: GenServer.cast/2
, GenServer.call/3
, and sending
messages via Kernel.send/2
.
If you haven't already acquainted yourself with GenServers and how to supervise them, see the official Elixir guides:
Let's begin with a fresh client
defmodule MyApp.GenServerClient do
use Slipstream
def start_link(opts) do
Slipstream.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl Slipstream
def init(opts) do
{:ok, connect!(opts)}
end
end
Already we are taking advantage of Slipstream.start_link/3
(which is roughly
the same as GenServer.start_link/3
) and Slipstream.init/1
(which is
roughly the same as GenServer.init/1
except that you must return a
Slipstream.Socket.t/0
).
Let's add a clause for handling GenServer.cast/2
s
(8d5837b
)
@impl Slipstream
def handle_cast({:join, topic, params}, socket) do
{:noreply, join(socket, topic, params)}
end
This clause will accept casts telling the client to join a topic and perform
the join within the client's process. Notice how Slipstream.handle_cast/2
looks exactly the same as GenServer.handle_cast/2
. The two are completely
compatible and all returns accepted by the GenServer callback are accepted by
Slipstream as well.
If one were to ask the client to join a topic, they need only
GenServer.cast/2
it
iex> GenServer.cast(MyApp.GenServerClient, {:join, "rooms:lobby", %{}})
:ok
Now let's try an implementation to handle GenServer.call/3
requests. Calling
is a synchronous action for which the caller will block until a reply is
received. This is the only callback in the set of Slipstream
callbacks which
can produce a reply-tuple ({:reply, reply, socket}
). Let's start with
something simple like a ping request
(fef68cc
)
@impl Slipstream
def handle_call(:ping, _from, socket) do
{:reply, :pong, socket}
end
This example doesn't really do very much. If we call our client with a
:ping
request, we expect a :pong
result.
iex> GenServer.call(MyApp.GenServerClient, :ping)
:pong
Let's try a more interesting GenServer.call/3
feature: the client replying
asynchronously. Surely the caller blocks while calling GenServer.call/3
, but
the server which hears the call does not necessarily have to respond in a
blocking fashion. With a combination of a noreply tuple ({:noreply, socket}
)
and GenServer.reply/2
, we can provide an API for synchronously requesting a
join
(d878f90
)
@impl Slipstream
def handle_call({:join, topic, params}, from, socket) do
socket =
socket
|> assign(:join_request, from)
|> join(topic, params)
{:noreply, socket}
end
@impl Slipstream
def handle_join(_topic, response, socket) do
GenServer.reply(socket.assigns.join_request, {:ok, response})
{:ok, socket}
end
And now we may call from another process
iex> GenServer.call(MyApp.GenServerClient, {:join, "rooms:lobby", %{}})
{:ok, %{}}
And only receive a reply when the topic has been joined.
Finally, the GenServer.handle_info/2
callback provides a catch-all callback
for handling any messages sent to the server which are not calls or casts.
This callback works asynchronously like GenServer.handle_cast/2
, but can
be triggered by another process performing a Kernel.send/2
instead of a
GenServer.cast/2
.
We might write a client which uses :timer.send_interval/2
,
Process.send_after/4
, or just plain Kernel.send/2
to remind itself to do
work later, or another process may wish to send the client a message to
alter its behavior. We'll take the approach of that last suggestion with this
callback implementation for Slipstream.handle_info/2
(63d3759
)
@impl Slipstream
def handle_info(:conclude_subscription, socket) do
{:stop, :normal, disconnect(socket)}
end
And we'll revisit that use Slipstream
invocation to add
use Slipstream, restart: :temporary
This will ensure that when we tell the client to clean up and disconnect from its subscription that it should shutdown and not be restarted.
When the client hears a message :conclude_subscription
, it will disconnect
gracefully and shut down the GenServer
process. We can trigger this with
iex> MyApp.GenServerClient |> Process.whereis() |> send(:conclude_subscription)
:conclude_subscription
Bonus
As an added bonus, we'll discuss the feature of continues and how to use
Slipstream.handle_continue/2
to schedule a block of work to come next.
Since OTP 21, the GenServer.handle_continue/2
callback has provided a way
to immediately schedule the next block of work without performing a
send(self(), {:next_work, params})
message.
You may wish to make use of this feature by scheduling the connection request
to occur after Slipstream.init/1
, soas the application supervision tree
may start up quickly, in the case where a client would have to do a large
amount of work before connecting.
@impl Slipstream
def init(config) do
socket = new_socket() |> assign(:config, config)
{:ok, socket, {:continue, :connect}}
end
@impl Slipstream
def handle_continue(:connect, socket) do
# do some expensive work...
{:noreply, connect!(socket, socket.assigns.config)}
end
This will make the call to Slipstream.connect!/2
occur after the conclusion
of the Slipstream.init/1
callback.