Skip to content
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

Simple TURN client #67

Merged
merged 13 commits into from
Apr 4, 2017
Merged

Simple TURN client #67

merged 13 commits into from
Apr 4, 2017

Conversation

arkgil
Copy link
Contributor

@arkgil arkgil commented Mar 28, 2017

Extended client process to issue allocate and refresh requests and react to responses. It also includes retries when stale nonce or unathorized errors are returned from the server. Also moved STUN/TURN processing to Jerboa.Client.Protocol module.

def bind(client) do
GenServer.call(client, :bind)
GenServer.call(client, :bind, 2 * timeout())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 * timeout() doesn't convey any idea "why" you're doing that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I don't know why it it 2*. What do you recommend, leave timeout() or go with the default GenServer timeout? If we go with the default, it doesn't make much sense to configure timeout at all.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's no reason at all with changing the timeouts I would get rid of it entirrely.

@moduledoc false

## Pure functions which construct requests or indications
## and processes responses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process not processes

{:ok, new_state} ->
{{:ok, new_state.mapped_address}, empty_transaction(new_state)}
error ->
{error, state}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong alignment

end

@spec eval_bind_resp(Worker.state)
:: {{:ok, mapped_address :: Client.address}, Worker.state}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like that the :ok or :error are in the inner tuple - is there any reason for that?

This results in the following code: {{:error, :bad_response}, worker_state} = Protocol.eval_bind_resp(..)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the inner tuple is the result returned to the caller of Jerboa.Client.bind/allocate/refresh..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to pass up what was returned from the Jerboa.Client.bind/allocate/refresh..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, when someone calls Jerboa.Client.bind it will internally call Protocol.eval_bind_resp, which returns a two elemenet tuple (from the Client module point of view): {result, new_state} - this way we just extract these two values, and return {:reply, result, new_state} from the handle_call/3 callback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

|> Params.put_attr(%Nonce{value: state.nonce})
end

require Logger
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require in the middle of a file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing line from debugging, going to remove that.

|> Protocol.eval_bind_resp()
{:reply, result, new_state}
end
def handle_call(:allocate, _, state) do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmhm that function could be better.. to many levels..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, probably with would make this prettier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately no, I can't solve this using with, because I won't be able to return new_state from one of the else clauses. To be honest, I have no idea how to refactor it at the moment.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 if state.relayed_address do
       {:reply, {:ok, state.relayed_address}, state}
 else
    {result, new_state} = request_allocation(state)
    {:reply, result, new_state}
end

why not that way and updating the timer in the request_allocation/1.

Another alternative I can spot is:

 if state.relayed_address do
       {:reply, {:ok, state.relayed_address}, state}
 else
    {result, new_state} = request_allocation(state)
    {:reply, result, maybe_update_lifetime_timer(result, new_state)}
end

socket = socket(state)
{address, port} = server(state)
:ok = UDP.send(socket, address, port, req)
{:ok, {^address, ^port, response}} = UDP.recv(socket, 0, timeout())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everytime you need timeout you take it our from the App env? no no no, that's not that good idea.. why not putting it into the server's state?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have thought about it too. Is it that much overhead of calling application's env? IIRC these are just ETS tables. I know that when we issue many such calls it'd become the problem. I'm gonna fix that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true - it shouldn't be that bad.

|> Protocol.eval_allocate_resp()
case result do
:retry ->
request_allocation(new_state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Infinite loop if we fail to perform an allocation?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about introducing a retries counter? So that we fail after a certain number of tries. The call to client process would probably fail due to a timeout, but this will ensure that process doesn't get stuck in this loop.

Another reasonable solution would be to fail just after one retry. We retry a request when we get initial unauthorized response, which is completely fine, but subsequent requests shouldn't result in such response.

We also retry when the response is stale_nonce, in which case the next request should succeed. If it doesn't there may be two reasons:

  • the server is dumb/misconfigured/non-compliant with RFC, and just returns stale_nonce because something bad happens - in this case it is totally fine to not retry
  • the server has really small nonce expiration time, and due to network conditions our request arrived after the new nonce has expired - I don't think it is likely to happen (it wouldn't be reasonable from the server side) but we can't assume it won't happen at some point. In this case a next retry would be justified, but we can just return stale_nonce error to the caller and assume that he'll retry manually.

What do you think is the most reasonable solution here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we need to distinguish situations when we want to retry because there's no response (cases when we suspect network failure) from the cases where according to the protocol we could retry.

In the first case the counter looks ok to me (providing the retires are at least logged).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think logging is what we should add to the client before even proceeding with next features.

|> Protocol.eval_refresh_resp()
case result do
:retry ->
request_refresh(new_state)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initinite loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment as above applies, but here we only retry on stale_nonce.

@rslota
Copy link
Contributor

rslota commented Mar 31, 2017

@arkgil Could you please rebase this branch onto master right after #82 is merged?

@arkgil
Copy link
Contributor Author

arkgil commented Mar 31, 2017

Yes, that shouldn't be a problem. I don't think the client code will change at all

@arkgil arkgil assigned arkgil and unassigned mentels Mar 31, 2017
Arkadiusz Gil added 10 commits March 31, 2017 14:13
Use Params API, disable timeouts on indications,
use Format.decode! to crash on undecodable messages,
store mapped address in client's state.
Until we run Jerboa in real world, we can't assume
what timeout is sufficient.
Default retries count for requests is 1.
@arkgil
Copy link
Contributor Author

arkgil commented Mar 31, 2017

I think that last thing we need here is some kind of debug logging, and after that we can merge. It has already became too big.

@arkgil
Copy link
Contributor Author

arkgil commented Apr 3, 2017

I think it's ready to be merged (unless @rslota or @erszcz want to check it).

@rslota
Copy link
Contributor

rslota commented Apr 3, 2017

I think it's ready to be merged (unless @rslota or @erszcz want to check it).

Looks good to me.

@arkgil
Copy link
Contributor Author

arkgil commented Apr 4, 2017

I spotted a bug, need a bit more work on that.

@jerboabot
Copy link
Collaborator

Ebert has finished reviewing this Pull Request and has found:

  • 4 possible new issues (including those that may have been commented here).

But beware that this branch is 4 commits behind the esl:master branch, and a review of an up to date branch would produce more accurate results.

You can see more details about this review at https://ebertapp.io/github/esl/jerboa/pulls/67.

@arkgil
Copy link
Contributor Author

arkgil commented Apr 4, 2017

Alright, now everything is fine. Please merge it if you don't see any more issues 🙂

@rslota
Copy link
Contributor

rslota commented Apr 4, 2017

Still waiting for @mentels to click "Approve" :)

@mentels mentels merged commit d8e6a0d into master Apr 4, 2017
@arkgil arkgil deleted the simple-turn-client branch April 25, 2017 08:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants