Use different failover mechanism on Linux

Instead of always doubling the request, first we wrap the request with a
failover timeout, 500ms, which is an average time for a normal request.
If this request failed, trigger re-bootstrapping and retry the request.
This commit is contained in:
Cuong Manh Le
2023-05-13 12:18:26 +07:00
committed by Cuong Manh Le
parent d57c1d6d44
commit 56d8dc865f
+16 -66
View File
@@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"runtime" "runtime"
"time"
"github.com/miekg/dns" "github.com/miekg/dns"
) )
@@ -103,79 +104,28 @@ func (r *dohResolver) doRequest(req *http.Request) (*http.Response, error) {
return c.Do(req) return c.Do(req)
} }
const failoverTimeout = 500 * time.Millisecond
// doRequestWithFailover is like doRequest, but wrap the request with initial timeout.
// If the first request failed, it's likely that the transport was broken, then trigger
// re-bootstrapping and retry the request.
func (r *dohResolver) doRequestWithFailover(req *http.Request) (*http.Response, error) { func (r *dohResolver) doRequestWithFailover(req *http.Request) (*http.Response, error) {
// To deal with network changes, for example, connect/disconnect to VPN, c := http.Client{Transport: r.transport}
// We use two clients:
//
// - mainClient: use the current transport.
// - failoverClient: use a clone of the current transport.
//
// Two clients will perform the requests concurrently, but with mainClient
// started first. So in normal condition, mainClient is likely to return first,
// and we will use its result. In case of mainClient failed, we trigger the
// re-bootstrapping process, and use the result from failover client.
mainClient := http.Client{Transport: r.transport}
failoverClient := http.Client{}
if r.isDoH3 { if r.isDoH3 {
if r.http3RoundTripper == nil { if r.http3RoundTripper == nil {
return nil, errors.New("DoH3 is not supported") return nil, errors.New("DoH3 is not supported")
} }
// TODO: figure out how to clone DOH3 round tripper? c.Transport = r.http3RoundTripper
mainClient.Transport = r.http3RoundTripper
failoverClient.Transport = r.http3RoundTripper
} else {
failoverClient.Transport = r.transport.Clone()
} }
ctx, cancel := context.WithTimeout(context.Background(), failoverTimeout)
done := make(chan struct{}) defer cancel()
defer close(done) resp, err := c.Do(req.WithContext(ctx))
if err == nil {
type result struct { return resp, err
resp *http.Response
err error
} }
r.uc.ReBootstrap()
respCh := make(chan result) c.Transport = r.uc.transport
doRequest := func(client *http.Client) { return c.Do(req)
resp, err := client.Do(req)
select {
case respCh <- result{resp: resp, err: err}:
case <-done:
if client == &mainClient && err != nil {
r.uc.ReBootstrap()
}
if resp != nil {
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
}
}
}
mainClientStarted := make(chan struct{})
go func() {
// Notify failoverClient that mainClient started.
close(mainClientStarted)
doRequest(&mainClient)
}()
go func() {
// Wait mainClient started first.
<-mainClientStarted
doRequest(&failoverClient)
}()
var (
resp *http.Response
err error
)
for range []*http.Client{&mainClient, &failoverClient} {
res := <-respCh
if res.err == nil {
resp = res.resp
break
}
err = res.err
}
return resp, err
} }
func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) { func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) {