-
Notifications
You must be signed in to change notification settings - Fork 55
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
feat: create Happy Eyeballs dialer #176
Conversation
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.
Thanks! I have added some comments, also I would suggest to update the PR title to feat: add HappyEyeballsDialer
// Channel to wait for before a new dial attempt. It starts | ||
// with a closed channel that doesn't block because there's no | ||
// wait initially. | ||
var attemptDelayCh <-chan struct{} = newClosedChan() |
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.
nit it seems this is the only place newClosedChan()
is used. Maybe we can simplify close
the channel here and deletenewClosedChan
function to save some lines?
var attemptDelayCh <-chan struct{} = newClosedChan() | |
attemptDelayCh := make(chan struct{}) | |
close(attemptDelayCh) |
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.
Your code doesn't work because attemptDelayCh
must be of type <-chan struct{}
, not chan struct{}
, otherwise the assignment attemptDelayCh = waitCtx.Done()
doesn't compile.
Instead of creating a channel, closing, and then assigning to attemptDelayCh
, I found it better to keep that logic out of the core logic, which is already complicated.
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.
SG, maybe var attemptDelayCh <-chan struct{} = make(chan struct{})
would work here?
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.
I tried that too 😅
The problem is that you can't close a read-only channel.
transport/happyeyeballs.go
Outdated
resolutionDelayCtx, cancelResolutionDelay := context.WithTimeout(searchCtx, 50*time.Millisecond) | ||
defer cancelResolutionDelay() | ||
readyToDialCh = resolutionDelayCtx.Done() |
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 these lines be simplified with the following? Because we already have a case <-searchCtx.Done()
statement in the select
below, which will be triggered once searchCtx
is cancelled. In addition, if we linked searchCtx
to readyToDialCh
, will there be any potential race conditions, because both case <-searchCtx.Done()
and case <-readyToDialCh
will be ready when searchCtx
is cancelled?
resolutionDelayCtx, cancelResolutionDelay := context.WithTimeout(searchCtx, 50*time.Millisecond) | |
defer cancelResolutionDelay() | |
readyToDialCh = resolutionDelayCtx.Done() | |
readyToDialCh = time.Deplay(50*time.Millisecond) |
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.
I'm assuming you meant time.NewTimer(50*time.Milisecond).C
. I wasn't sure if we needed to drain the Timer.C
channel, but it seems it's buffered and we don't need to?
In any case, I tried based on your suggestion, but the type of channel is different (<-chan time.Time
), so it requires more changes, not really simpler. Also, the timer sends the time once, versus a context closes the channel, which is easier to reason about (a read will never block).
Another important point is that I still need to stop the timer, so I need to pass it to the goroutine to call Stop in case the dial returns before the timer triggers.
On the race condition, that's a valid point. A cancellation will potentially trigger an extra dial. I tried different ways to trigger, but couldn't. In any case, I changed the code to use the background context instead.
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.
Comments addressed
// Channel to wait for before a new dial attempt. It starts | ||
// with a closed channel that doesn't block because there's no | ||
// wait initially. | ||
var attemptDelayCh <-chan struct{} = newClosedChan() |
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.
Your code doesn't work because attemptDelayCh
must be of type <-chan struct{}
, not chan struct{}
, otherwise the assignment attemptDelayCh = waitCtx.Done()
doesn't compile.
Instead of creating a channel, closing, and then assigning to attemptDelayCh
, I found it better to keep that logic out of the core logic, which is already complicated.
transport/happyeyeballs.go
Outdated
resolutionDelayCtx, cancelResolutionDelay := context.WithTimeout(searchCtx, 50*time.Millisecond) | ||
defer cancelResolutionDelay() | ||
readyToDialCh = resolutionDelayCtx.Done() |
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.
I'm assuming you meant time.NewTimer(50*time.Milisecond).C
. I wasn't sure if we needed to drain the Timer.C
channel, but it seems it's buffered and we don't need to?
In any case, I tried based on your suggestion, but the type of channel is different (<-chan time.Time
), so it requires more changes, not really simpler. Also, the timer sends the time once, versus a context closes the channel, which is easier to reason about (a read will never block).
Another important point is that I still need to stop the timer, so I need to pass it to the goroutine to call Stop in case the dial returns before the timer triggers.
On the race condition, that's a valid point. A cancellation will potentially trigger an extra dial. I tried different ways to trigger, but couldn't. In any case, I changed the code to use the background context instead.
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.
The code LGTM, with some questions to be resolved. But the code flow seems a little bit complicated, hopefully we did not miss any race conditions or edge cases.
// Indicates to attempts that the dialing process is done, so they don't get stuck. | ||
ctx, dialDone := context.WithCancel(ctx) | ||
defer dialDone() |
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.
Here I mean we did not do anything special with dialDone
in the last review. Why not just simply using the ctx
parameter directly in the code below?
// Indicates to attempts that the dialing process is done, so they don't get stuck. | |
ctx, dialDone := context.WithCancel(ctx) | |
defer dialDone() |
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.
There's no guarantee that the input context will be cancelled. In this code, I cancel the context to make sure all subtasks are notifed and can complete.
If I remove the dialDone
, the subtasks may hang for a while.
// Channel to wait for before a new dial attempt. It starts | ||
// with a closed channel that doesn't block because there's no | ||
// wait initially. | ||
var attemptDelayCh <-chan struct{} = newClosedChan() |
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.
SG, maybe var attemptDelayCh <-chan struct{} = make(chan struct{})
would work here?
} | ||
|
||
func TestHappyEyeballsStreamDialer_DialStream(t *testing.T) { | ||
t.Run("Works with IPv4 hosts", func(t *testing.T) { |
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.
What's the benefit of using t.Run
instead of defining a test function func TestIPv4HostShouldWork() {}
?
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.
transport/happyeyeballs_test.go
Outdated
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type colletcStreamDialer struct { |
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.
type colletcStreamDialer struct { | |
type collectStreamDialer struct { |
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.
Fixed.
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.
I agree the flow is a bit complicated. I couldn't figure out a simpler way (it was a lot worse before). At least I have ~100% coverage for this code.
FYI, I generalized the API. Now you can customize the resolution phase to get v1 or v2 behavior. You can also inject hard-coded IPs, which will be quite useful.
I added documentation examples that are helpful illustrations.
// Indicates to attempts that the dialing process is done, so they don't get stuck. | ||
ctx, dialDone := context.WithCancel(ctx) | ||
defer dialDone() |
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.
There's no guarantee that the input context will be cancelled. In this code, I cancel the context to make sure all subtasks are notifed and can complete.
If I remove the dialDone
, the subtasks may hang for a while.
// Channel to wait for before a new dial attempt. It starts | ||
// with a closed channel that doesn't block because there's no | ||
// wait initially. | ||
var attemptDelayCh <-chan struct{} = newClosedChan() |
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.
I tried that too 😅
The problem is that you can't close a read-only channel.
transport/happyeyeballs_test.go
Outdated
"github.com/stretchr/testify/require" | ||
) | ||
|
||
type colletcStreamDialer struct { |
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.
Fixed.
} | ||
|
||
func TestHappyEyeballsStreamDialer_DialStream(t *testing.T) { | ||
t.Run("Works with IPv4 hosts", func(t *testing.T) { |
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.
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.
Thanks for the contribution!
No description provided.