Skip to content

Commit

Permalink
documentation on retry-mechanisms and gun choice
Browse files Browse the repository at this point in the history
  • Loading branch information
the-mikedavis committed Feb 4, 2021
1 parent f65f4d3 commit 561b84a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 4 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.3.3 - 2021-02-04

### Added

- increased documentation around retry mechanisms
- added a page on the choice of `:gun` as the low-level webscket client

## 0.3.2 - 2021-02-03

### Fixed
Expand Down
39 changes: 39 additions & 0 deletions guides/why_gun.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Why :gun?

Slipstream is built off of [`:gun`](https://hex.pm/packages/gun),
an HTTP and websocket client from the wonderful
[`@ninenines`](https://github.com/ninenines) family of projects.
(Others include [`:cowboy`](https://hex.pm/packages/cowboy) and
[`:ranch`](https://hex.pm/packages/ranch), which power Phoenix.)

`:gun` is battle-tested and fun to use, but the
main feature we wanted gun for is that it is not
[`:websocket_client`](https://hex.pm/packages/websocket_client).

Prior to Slipstream, we at
[`NFIBrokerage`](https://github.com/NFIBrokerage) used
[`phoenix_gen_socket_client`](https://hex.pm/packages/phoenix_gen_socket_client)
and [`phoenix_client`](https://hex.pm/packages/phoenix_client), which are both
backed by [`:websocket_client`](https://hex.pm/packages/websocket_client). Once
we started using phoenix channel clients in our production
services en masse, we found an odd bug we believe to be a [known
issue](https://github.com/sanmiguel/websocket_client/issues/62) in
`:websocket_client` around the RFC-conformance of close frames. This
closing issue would manifest itself as zombie procesess on the server-side
(specifically, `:cowboy_clear.connection_process/4` processes) which would
never release process memory.

This isn't likely a common problem for most phoenix websocket client users, but
at the time we were initially testing out a framework for fueling front-ends
and keeping them up-to-date with a library called Haste, which we intend on
open-sourcing soon. Haste uses websockets rather aggressively, connecting and
disconnecting them very quickly and moving large amounts of data over-the-wire.

In addition to loading pages of data over-the-wire on the websocket connection,
the `Phoenix.Channel` on the server-side would attempt to determine if changes
made in the database would affect rows on a users table. This resulted in a
large numbers of messages being sent to the `Phoenix.Channel`, which prevented
process hibernation, leading to a very large process memory. The stacking up
of this process memory and the zombie-like state these processes were left
in when `:websocket_client` attempted to disconnect would quickly overwhelm
the backend server hosting the data, leading to out-of-memory crashes.
68 changes: 65 additions & 3 deletions lib/slipstream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,57 @@ defmodule Slipstream do
:hello
iex> GenServer.call(MyClient, :foo)
{:ok, :bar}
## Retry Mechanisms
Slipstream emulates the official `phoenix.js` package with its reconnection
and re-join features. `Slipstream.Configuration` allows configuration of the
back-off times with the `:reconnect_after_msec` and `:rejoin_after_msec`
lists, respectively.
To take advantage of these built-in mechanisms, a client must be written
in the asynchronous GenServer-like manner and must use the `reconnect/1` and
`rejoin/3` functions in its `c:Slipstream.handle_disconnect/2` and
`c:Slipstream.handle_topic_close/3` callbacks, respectively. Note that the
default implementation of these callbacks invokes these functions, so a client
which does not explicitly define these callbacks will retry connection and
joins.
Take care to handle the `:left` case of `c:Slipstream.handle_topic_close/3`.
In the case that a client attempts to leave a topic with `leave/2`, the
callback will be invoked with a `reason` of `:left`. The default
implementation of `c:Slipstream.handle_topic_close/3` makes this distinction
and simply no-ops on channel leaves.
defmodule MyClientWithRetry do
use Slipstream
def start_link(config) do
Slipstream.start_link(__MODULE__, config, name: __MODULE__)
end
@impl Slipstream
def init(config), do: connect(config)
@impl Slipstream
def handle_connect(socket) do
{:ok, join(socket, "rooms:lobby", %{user_id: 1})}
end
@impl Slipstream
def handle_disconnect(_reason, socket) do
reconnect(socket)
end
@impl Slipstream
def handle_topic_close(_topic, :left, socket) do
{:ok, socket}
end
def handle_topic_close(topic, _reason, socket) do
rejoin(socket, topic)
end
end
"""

alias Slipstream.{Commands, Events, Socket}
Expand Down Expand Up @@ -568,15 +619,26 @@ defmodule Slipstream do
when new_socket: Socket.t()

@doc """
Invoked when a channel has been closed by the remote server
Invoked when a join has concluded
This callback will be invoked in a few cases:
- the remote `Phoenix.Channel` crashes, e.g. by a raised error
- the client successfully leaves the topic with `leave/2`
In the case that the client has left the topic, `reason` will simply be
`:left`. If the remote channel crashes, the `reason` will be an error tuple
`{:error, params :: json_serializable()}` where `params` is the message
sent from the remote channel on chrash.
The default implementation of this callback attempts to re-join the
last-joined topic.
last-joined topic whenever `reason != :left`. If the reason is `:left`, the
default implementation will no-op by returning `{:ok, socket}`.
## Examples
@impl Slipstream
def handle_topic_close(topic, _message, socket) do
def handle_topic_close(topic, _reason, socket) do
{:ok, socket} = rejoin(socket, topic)
end
"""
Expand Down
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ defmodule Slipstream.MixProject do
extras: [
"CHANGELOG.md",
"guides/telemetry.md",
"guides/implementation.md"
"guides/implementation.md",
"guides/why_gun.md"
],
groups_for_extras: [
Guides: Path.wildcard("guides/*.md")
Expand Down

0 comments on commit 561b84a

Please sign in to comment.