Skip to content

Commit

Permalink
*: add a 1 minute HTTP inactivity timeout
Browse files Browse the repository at this point in the history
As described in issue github-release#26, sometimes the Github server seems to disappear
and Go's net/http doesn't seem to notice. This makes github-release(1) hang
indefinitely (or at least a very long time, until the OS decides to close
the TCP conn).

Attempt to work around this by setting a read/write timeout.

Updates github-release#26.
  • Loading branch information
aktau committed Apr 3, 2017
1 parent 1139b43 commit febc683
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 2 deletions.
4 changes: 2 additions & 2 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func DoAuthRequest(method, url, mime, token string, headers map[string]string, b
return nil, err
}

resp, err := http.DefaultClient.Do(req)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -149,7 +149,7 @@ func (c Client) getPaginated(uri string) (io.ReadCloser, error) {
v.Set("access_token", c.Token)
}
u.RawQuery = v.Encode()
resp, err := http.Get(u.String())
resp, err := client.Get(u.String())
if err != nil {
return nil, err
}
Expand Down
76 changes: 76 additions & 0 deletions github/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package github

import (
"context"
"net"
"net/http"
"time"
)

// Github's HTTP endpoint for asset uploads is flaky. Often it will drop a
// TCP connection *. The HTTP POST will just hang indefinitely. To solve
// this without using absolute deadlines (which would be hard to predict as
// it depends on the size of the asset and the speed of the users'
// connection), we use a modified http.Client which resets a timeout every
// time the kernel accepts a read/write on the socket. Doing so basically
// creates a sort of "inactivity watcher".
//
// We can't use the http.Client.Timeout field, as that's an absolute timeout
// which doesn't get reset whenever there's some activity.
//
// * At least that's what I think is happening, I have never observed this
// myself since I never upload big assets, see issue
// http://github.com/aktau/github-release/issues/26.

// HTTPReadWriteTimeout is the read/write timeout after which connections
// will be closed.
const HTTPReadWriteTimeout = 1 * time.Minute

// dialer for use by the transport below, initialized like the net/http
// dialer.
var dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}

// transport for use by the http.Client below.
//
// TODO: enable HTTP/2 transport as documented in net/http.
var transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) {
return newWatchdogConn(dialer.DialContext(ctx, netw, addr))
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
ResponseHeaderTimeout: 1 * time.Minute,
}

// client is an HTTP client suited for use with GitHub. It includes watchdog
// functionality that will break of an upload/download attempt after
// HTTPReadWriteTimeout.
var client = &http.Client{Transport: transport}

// watchdogConn wraps a net.Conn, on every read and write it sets a timeout.
// If no bytes are read or written in that time, the connection is closed.
type watchdogConn struct {
net.Conn
timeout time.Duration // The amount of time to wait between reads/writes on the connection before cancelling it.
}

func newWatchdogConn(conn net.Conn, err error) (net.Conn, error) {
return &watchdogConn{Conn: conn, timeout: HTTPReadWriteTimeout}, err
}

func (c *watchdogConn) Read(b []byte) (n int, err error) {
c.SetReadDeadline(time.Now().Add(c.timeout))
return c.Conn.Read(b)
}

func (c *watchdogConn) Write(b []byte) (n int, err error) {
c.SetWriteDeadline(time.Now().Add(c.timeout))
return c.Conn.Write(b)
}

0 comments on commit febc683

Please sign in to comment.