-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implement backoff for reconnect #112
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,14 @@ | ||
## Next | ||
### Enhancements | ||
- Added `backoff` feature for reconnecting | ||
- `send_frame/2` is no longer processed during `opening`, ie. it is queued until `connected` or failure to connect. | ||
|
||
### Breaking Changes | ||
- Connection is always established asynchronously to process init | ||
- `async` option is removed since it's now redundant | ||
- `handle_initial_conn_failure` option is removed since it's no longer applicable | ||
Comment on lines
+7
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This actually ruins one of the ways I'm using WebSockex myself. 😢 In my use case, there are a couple of other WebSockex processes that depend on the connection being open before they start then when things disconnect the supervisor uses a Originally, reconnecting was actually more of a feature request that I challenged myself to do. I rarely used it myself. 😣 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, the reason why I removed sync connect is because as far as I know, it's bad practice to handle a potentionally long task in the init stage of a special process, and e.g. in a
You can also observe the result of this by making a simple I prefer not to give users (of this library) footguns and unexpected behaviour which is why I decided to remove the ability to do this. I can see that the biggest issue is actually the combination of sync connect and reconnect and that these 2 features are actually at odds of co-existing at the same time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This can definitely be true in the Application supervisor, but sometimes even that is necessary. I don't think you can consider it a bad practice, but it can be annoying to debug. On the other hand, it can also fit really well into the failure as feature mentality. But there are people who want "footguns" simply because it fits into their application flow easier. That being said, But my main argument will be that |
||
- `handle_disconnect/2` no longer accepts `:reconnect` as a result. This is replaced with `:backoff` with a required timeout. | ||
|
||
## 0.4.3 | ||
### Enhancements | ||
- Added `ssl_options` documentation | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
defmodule BackoffClient do | ||
use WebSockex | ||
require Logger | ||
|
||
def start_link(opts \\ []) do | ||
WebSockex.start_link("wss://echo.websocket.org/?encoding=text", __MODULE__, :fake_state, opts) | ||
end | ||
|
||
@spec echo(pid, String.t) :: :ok | ||
def echo(client, message) do | ||
Logger.info("Sending message: #{message}") | ||
WebSockex.send_frame(client, {:text, message}) | ||
end | ||
|
||
def handle_connect(_conn, state) do | ||
Logger.info("Connected!") | ||
{:ok, state} | ||
end | ||
|
||
def handle_frame({:text, "Can you please reply yourself?" = msg}, :fake_state) do | ||
Logger.info("Received Message: #{msg}") | ||
msg = "Sure can!" | ||
ref = make_ref() | ||
Logger.info("Sending message: #{msg} with ref #{inspect(ref)}") | ||
{:reply, {:text, msg}, ref, :fake_state} | ||
end | ||
def handle_frame({:text, "Close the things!" = msg}, :fake_state) do | ||
Logger.info("Received Message: #{msg}") | ||
{:close, :fake_state} | ||
end | ||
def handle_frame({:text, msg}, :fake_state) do | ||
Logger.info("Received Message: #{msg}") | ||
{:ok, :fake_state} | ||
end | ||
|
||
def handle_disconnect(%{attempt_number: attempt, reason: reason}, state) do | ||
Logger.warn("Websocket connection failed because: #{inspect(reason)}. Attempt: #{attempt}") | ||
{:backoff, 1_000, state} | ||
end | ||
|
||
def handle_send_result(result, frame, send_key, state) do | ||
Logger.debug("Send result for key: #{inspect(send_key)}, frame: #{inspect(frame)}, result: #{inspect(result)}") | ||
{:ok, state} | ||
end | ||
end | ||
|
||
{:ok, pid} = BackoffClient.start_link() | ||
|
||
BackoffClient.echo(pid, "Yo Homies!") | ||
BackoffClient.echo(pid, "This and That!") | ||
BackoffClient.echo(pid, "Can you please reply yourself?") | ||
|
||
Process.sleep 1000 | ||
|
||
BackoffClient.echo(pid, "Close the things!") | ||
|
||
Process.sleep 1500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually a breaking change, and potentially a large one in my opinion.
I'm torn on this. I understand that it could make the library more convenient, but it could also cause a message to be sent prematurely before some kind of required connection setup was finished.
Adding another method or keyword option would be my preference but that means more stuff to maintain. It could be useful though. I remain somewhat on the edge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on this? Maybe I'm missing something but I'm not sure when a message could be sent prematurely because of this?
Initially I had failing tests since they currently assume that when the process is started, the connection is connected and able to send messages. Before I realized that I could make this change with selective receive so that the
send_frame
message waits in the inbox until the connection is connected, I added a featureWebSockex.await_status(pid, :connected)
so that you could do the following:However I removed this and instead relied on selective receive because it means less work for the user and one less change in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handle_connect
is used to queue up initial messages. By casting to itself, a process can queue up messages to send in a specific order after startup.By handling
send_frame
after the connection is established, we actually put ourselves at much higher risk for a race condition. A frame could be sent before the initial connection messages were sent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, I have 2 thoughts on this.
I'm just exploring how this could work better ignoring breaking changes. Maybe first read my comment on
handle_send_result
. If instead ofsend_frame
we havehandle_send_result
andhandle_call
which would allow a user to implement their ownsend_frame
, and we haveinit
, then the user could create a message queue in their state duringinit
for startup messages and thenhandle_call
could add messages to the queue as well and the queue can be processed in the right order starting fromhandle_connect
. I also think that addinghandle_continue
support would be useful here. In fact if we just addhandle_continue
then this can be used fromhandle_connect
to process initial messages beforesend_frame
messages by calling:continue
inhandle_connect
.Without making breaking changes, we need a way to wait for the connection to be established before calling
send_frame
. This can either be done by addingawait_status
as above, or by addinghandle_call
which has access to the current status. The user can theoretically keep track of the current status withhandle_connect
andhandle_disconnect
in their own state, and implementhandle_call
on top ofhandle_cast
, but it's quite a lot of extra stuff for each user to add which is already a solved problem in a plainGenServer
. On the one hand I kinda like addingawait_status
natively because it requires no extra work from the user, but on the other hand I'm really seeing that everything could be handled much better with more flexibility ifWebSockex
is on par withGenServer
, namely havinginit
,handle_call
,handle_continue
.I was trying to solve all the issues I ran into with adding backoff without adding a lot of other features, but it seems that we'd be better served by first adding the other features that we can then more easily build on here.