Skip to content

Commit

Permalink
os, net: define and use os.ErrDeadlineExceeded
Browse files Browse the repository at this point in the history
If an I/O operation fails because a deadline was exceeded,
return os.ErrDeadlineExceeded. We used to return poll.ErrTimeout,
an internal error, and told users to check the Timeout method.
However, there are other errors with a Timeout method that returns true,
notably syscall.ETIMEDOUT which is returned for a keep-alive timeout.
Checking errors.Is(err, os.ErrDeadlineExceeded) should permit code
to reliably tell why it failed.

This change does not affect the handling of net.Dialer.Deadline,
nor does it change the handling of net.DialContext when the context
deadline is exceeded. Those cases continue to return an error
reported as "i/o timeout" for which Timeout is true, but that error
is not os.ErrDeadlineExceeded.

Fixes #31449

Change-Id: I0323f42e944324c6f2578f00c3ac90c24fe81177
Reviewed-on: https://go-review.googlesource.com/c/go/+/228645
Run-TryBot: Ian Lance Taylor <[email protected]>
TryBot-Result: Gobot Gobot <[email protected]>
Reviewed-by: Filippo Valsorda <[email protected]>
  • Loading branch information
ianlancetaylor committed Apr 25, 2020
1 parent 396833c commit d422f54
Show file tree
Hide file tree
Showing 20 changed files with 196 additions and 108 deletions.
38 changes: 38 additions & 0 deletions doc/go1.15.html
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,25 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
</dd>
</dl>

<dl id="net"><dt><a href="/pkg/net/">net</a></dt>
<dd>
<p><!-- CL -->
If an I/O operation exceeds a deadline set by
the <a href="/pkg/net/#Conn"><code>Conn.SetDeadline</code></a>,
<code>Conn.SetReadDeadline</code>,
or <code>Conn.SetWriteDeadline</code> methods, it will now
return an error that is or wraps
<a href="/pkg/os#ErrDeadlineExceeded"><code>os.ErrDeadlineExceeded</code></a>.
This may be used to reliably detect whether an error is due to
an exceeded deadline.
Earlier releases recommended calling the <code>Timeout</code>
method on the error, but I/O operations can return errors for
which <code>Timeout</code> returns <code>true</code> although a
deadline has not been exceeded.
</p>
</dd>
</dl>

<dl id="net/http/pprof"><dt><a href="/pkg/net/http/pprof/">net/http/pprof</a></dt>
<dd>
<p><!-- CL 147598, 229537 -->
Expand Down Expand Up @@ -200,6 +219,25 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
</dd>
</dl>

<dl id="os"><dt><a href="/pkg/os/">os</a></dt>
<dd>
<p><!-- CL -->
If an I/O operation exceeds a deadline set by
the <a href="/pkg/os/#File.SetDeadline"><code>File.SetDeadline</code></a>,
<a href="/pkg/os/#File.SetReadDeadline"><code>File.SetReadDeadline</code></a>,
or <a href="/pkg/os/#File.SetWriteDeadline"><code>File.SetWriteDeadline</code></a>
methods, it will now return an error that is or wraps
<a href="/pkg/os#ErrDeadlineExceeded"><code>os.ErrDeadlineExceeded</code></a>.
This may be used to reliably detect whether an error is due to
an exceeded deadline.
Earlier releases recommended calling the <code>Timeout</code>
method on the error, but I/O operations can return errors for
which <code>Timeout</code> returns <code>true</code> although a
deadline has not been exceeded.
</p>
</dd>
</dl>

<dl id="reflect"><dt><a href="/pkg/reflect/">reflect</a></dt>
<dd>
<p><!-- CL 228902 -->
Expand Down
18 changes: 11 additions & 7 deletions src/internal/poll/fd.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,20 @@ func errClosing(isFile bool) error {
return ErrNetClosing
}

// ErrTimeout is returned for an expired deadline.
var ErrTimeout error = &TimeoutError{}
// ErrDeadlineExceeded is returned for an expired deadline.
// This is exported by the os package as os.ErrDeadlineExceeded.
var ErrDeadlineExceeded error = &DeadlineExceededError{}

// TimeoutError is returned for an expired deadline.
type TimeoutError struct{}
// DeadlineExceededError is returned for an expired deadline.
type DeadlineExceededError struct{}

// Implement the net.Error interface.
func (e *TimeoutError) Error() string { return "i/o timeout" }
func (e *TimeoutError) Timeout() bool { return true }
func (e *TimeoutError) Temporary() bool { return true }
// The string is "i/o timeout" because that is what was returned
// by earlier Go versions. Changing it may break programs that
// match on error strings.
func (e *DeadlineExceededError) Error() string { return "i/o timeout" }
func (e *DeadlineExceededError) Timeout() bool { return true }
func (e *DeadlineExceededError) Temporary() bool { return true }

// ErrNotPollable is returned when the file or socket is not suitable
// for event notification.
Expand Down
8 changes: 4 additions & 4 deletions src/internal/poll/fd_plan9.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (fd *FD) Close() error {
// Read implements io.Reader.
func (fd *FD) Read(fn func([]byte) (int, error), b []byte) (int, error) {
if fd.rtimedout.isSet() {
return 0, ErrTimeout
return 0, ErrDeadlineExceeded
}
if err := fd.readLock(); err != nil {
return 0, err
Expand All @@ -76,15 +76,15 @@ func (fd *FD) Read(fn func([]byte) (int, error), b []byte) (int, error) {
err = io.EOF
}
if isInterrupted(err) {
err = ErrTimeout
err = ErrDeadlineExceeded
}
return n, err
}

// Write implements io.Writer.
func (fd *FD) Write(fn func([]byte) (int, error), b []byte) (int, error) {
if fd.wtimedout.isSet() {
return 0, ErrTimeout
return 0, ErrDeadlineExceeded
}
if err := fd.writeLock(); err != nil {
return 0, err
Expand All @@ -94,7 +94,7 @@ func (fd *FD) Write(fn func([]byte) (int, error), b []byte) (int, error) {
n, err := fd.waio.Wait()
fd.waio = nil
if isInterrupted(err) {
err = ErrTimeout
err = ErrDeadlineExceeded
}
return n, err
}
Expand Down
2 changes: 1 addition & 1 deletion src/internal/poll/fd_poll_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (pd *pollDesc) wait(mode int, isFile bool) error {
if isFile { // TODO(neelance): wasm: Use callbacks from JS to block until the read/write finished.
return nil
}
return ErrTimeout
return ErrDeadlineExceeded
}

func (pd *pollDesc) waitRead(isFile bool) error { return pd.wait('r', isFile) }
Expand Down
2 changes: 1 addition & 1 deletion src/internal/poll/fd_poll_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func convertErr(res int, isFile bool) error {
case pollErrClosing:
return errClosing(isFile)
case pollErrTimeout:
return ErrTimeout
return ErrDeadlineExceeded
case pollErrNotPollable:
return ErrNotPollable
}
Expand Down
2 changes: 1 addition & 1 deletion src/internal/poll/fd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func execIO(o *operation, submit func(o *operation) error) (int, error) {
// IO is interrupted by "close" or "timeout"
netpollErr := err
switch netpollErr {
case ErrNetClosing, ErrFileClosing, ErrTimeout:
case ErrNetClosing, ErrFileClosing, ErrDeadlineExceeded:
// will deal with those.
default:
panic("unexpected runtime.netpoll error: " + netpollErr.Error())
Expand Down
3 changes: 1 addition & 2 deletions src/net/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package net
import (
"context"
"internal/nettrace"
"internal/poll"
"syscall"
"time"
)
Expand Down Expand Up @@ -141,7 +140,7 @@ func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, er
}
timeRemaining := deadline.Sub(now)
if timeRemaining <= 0 {
return time.Time{}, poll.ErrTimeout
return time.Time{}, errTimeout
}
// Tentatively allocate equal time to each remaining address.
timeout := timeRemaining / time.Duration(addrsRemaining)
Expand Down
5 changes: 2 additions & 3 deletions src/net/dial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package net
import (
"bufio"
"context"
"internal/poll"
"internal/testenv"
"io"
"os"
Expand Down Expand Up @@ -540,8 +539,8 @@ func TestDialerPartialDeadline(t *testing.T) {
{now, noDeadline, 1, noDeadline, nil},
// Step the clock forward and cross the deadline.
{now.Add(-1 * time.Millisecond), now, 1, now, nil},
{now.Add(0 * time.Millisecond), now, 1, noDeadline, poll.ErrTimeout},
{now.Add(1 * time.Millisecond), now, 1, noDeadline, poll.ErrTimeout},
{now.Add(0 * time.Millisecond), now, 1, noDeadline, errTimeout},
{now.Add(1 * time.Millisecond), now, 1, noDeadline, errTimeout},
}
for i, tt := range testCases {
deadline, err := partialDeadline(tt.now, tt.deadline, tt.addrs)
Expand Down
15 changes: 7 additions & 8 deletions src/net/dnsclient_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"context"
"errors"
"fmt"
"internal/poll"
"io/ioutil"
"os"
"path"
Expand Down Expand Up @@ -480,7 +479,7 @@ func TestGoLookupIPWithResolverConfig(t *testing.T) {
break
default:
time.Sleep(10 * time.Millisecond)
return dnsmessage.Message{}, poll.ErrTimeout
return dnsmessage.Message{}, os.ErrDeadlineExceeded
}
r := dnsmessage.Message{
Header: dnsmessage.Header{
Expand Down Expand Up @@ -993,7 +992,7 @@ func TestRetryTimeout(t *testing.T) {
if s == "192.0.2.1:53" {
deadline0 = deadline
time.Sleep(10 * time.Millisecond)
return dnsmessage.Message{}, poll.ErrTimeout
return dnsmessage.Message{}, os.ErrDeadlineExceeded
}

if deadline.Equal(deadline0) {
Expand Down Expand Up @@ -1131,7 +1130,7 @@ func TestStrictErrorsLookupIP(t *testing.T) {
}
makeTimeout := func() error {
return &DNSError{
Err: poll.ErrTimeout.Error(),
Err: os.ErrDeadlineExceeded.Error(),
Name: name,
Server: server,
IsTimeout: true,
Expand Down Expand Up @@ -1247,7 +1246,7 @@ func TestStrictErrorsLookupIP(t *testing.T) {
Questions: q.Questions,
}, nil
case resolveTimeout:
return dnsmessage.Message{}, poll.ErrTimeout
return dnsmessage.Message{}, os.ErrDeadlineExceeded
default:
t.Fatal("Impossible resolveWhich")
}
Expand Down Expand Up @@ -1372,7 +1371,7 @@ func TestStrictErrorsLookupTXT(t *testing.T) {

switch q.Questions[0].Name.String() {
case searchX:
return dnsmessage.Message{}, poll.ErrTimeout
return dnsmessage.Message{}, os.ErrDeadlineExceeded
case searchY:
return mockTXTResponse(q), nil
default:
Expand All @@ -1387,7 +1386,7 @@ func TestStrictErrorsLookupTXT(t *testing.T) {
var wantRRs int
if strict {
wantErr = &DNSError{
Err: poll.ErrTimeout.Error(),
Err: os.ErrDeadlineExceeded.Error(),
Name: name,
Server: server,
IsTimeout: true,
Expand Down Expand Up @@ -1415,7 +1414,7 @@ func TestDNSGoroutineRace(t *testing.T) {

fake := fakeDNSServer{rh: func(n, s string, q dnsmessage.Message, t time.Time) (dnsmessage.Message, error) {
time.Sleep(10 * time.Microsecond)
return dnsmessage.Message{}, poll.ErrTimeout
return dnsmessage.Message{}, os.ErrDeadlineExceeded
}}
r := Resolver{PreferGo: true, Dial: fake.DialContext}

Expand Down
10 changes: 5 additions & 5 deletions src/net/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ second:
return nil
}
switch err := nestedErr.(type) {
case *AddrError, addrinfoErrno, *DNSError, InvalidAddrError, *ParseError, *poll.TimeoutError, UnknownNetworkError:
case *AddrError, addrinfoErrno, *timeoutError, *DNSError, InvalidAddrError, *ParseError, *poll.DeadlineExceededError, UnknownNetworkError:
return nil
case *os.SyscallError:
nestedErr = err.Err
Expand Down Expand Up @@ -436,7 +436,7 @@ second:
goto third
}
switch nestedErr {
case poll.ErrNetClosing, poll.ErrTimeout, poll.ErrNotPollable:
case poll.ErrNetClosing, errTimeout, poll.ErrNotPollable, os.ErrDeadlineExceeded:
return nil
}
return fmt.Errorf("unexpected type on 2nd nested level: %T", nestedErr)
Expand Down Expand Up @@ -471,14 +471,14 @@ second:
return nil
}
switch err := nestedErr.(type) {
case *AddrError, addrinfoErrno, *DNSError, InvalidAddrError, *ParseError, *poll.TimeoutError, UnknownNetworkError:
case *AddrError, addrinfoErrno, *timeoutError, *DNSError, InvalidAddrError, *ParseError, *poll.DeadlineExceededError, UnknownNetworkError:
return nil
case *os.SyscallError:
nestedErr = err.Err
goto third
}
switch nestedErr {
case errCanceled, poll.ErrNetClosing, errMissingAddress, poll.ErrTimeout, ErrWriteToConnected, io.ErrUnexpectedEOF:
case errCanceled, poll.ErrNetClosing, errMissingAddress, errTimeout, os.ErrDeadlineExceeded, ErrWriteToConnected, io.ErrUnexpectedEOF:
return nil
}
return fmt.Errorf("unexpected type on 2nd nested level: %T", nestedErr)
Expand Down Expand Up @@ -627,7 +627,7 @@ second:
goto third
}
switch nestedErr {
case poll.ErrNetClosing, poll.ErrTimeout, poll.ErrNotPollable:
case poll.ErrNetClosing, errTimeout, poll.ErrNotPollable, os.ErrDeadlineExceeded:
return nil
}
return fmt.Errorf("unexpected type on 2nd nested level: %T", nestedErr)
Expand Down
41 changes: 27 additions & 14 deletions src/net/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ package net
import (
"context"
"errors"
"internal/poll"
"io"
"os"
"sync"
Expand Down Expand Up @@ -136,23 +135,22 @@ type Conn interface {
// SetReadDeadline and SetWriteDeadline.
//
// A deadline is an absolute time after which I/O operations
// fail with a timeout (see type Error) instead of
// blocking. The deadline applies to all future and pending
// I/O, not just the immediately following call to Read or
// Write. After a deadline has been exceeded, the connection
// can be refreshed by setting a deadline in the future.
// fail instead of blocking. The deadline applies to all future
// and pending I/O, not just the immediately following call to
// Read or Write. After a deadline has been exceeded, the
// connection can be refreshed by setting a deadline in the future.
//
// If the deadline is exceeded a call to Read or Write or to other
// I/O methods will return an error that wraps os.ErrDeadlineExceeded.
// This can be tested using errors.Is(err, os.ErrDeadlineExceeded).
// The error's Timeout method will return true, but note that there
// are other possible errors for which the Timeout method will
// return true even if the deadline has not been exceeded.
//
// An idle timeout can be implemented by repeatedly extending
// the deadline after successful Read or Write calls.
//
// A zero value for t means I/O operations will not time out.
//
// Note that if a TCP connection has keep-alive turned on,
// which is the default unless overridden by Dialer.KeepAlive
// or ListenConfig.KeepAlive, then a keep-alive failure may
// also return a timeout error. On Unix systems a keep-alive
// failure on I/O can be detected using
// errors.Is(err, syscall.ETIMEDOUT).
SetDeadline(t time.Time) error

// SetReadDeadline sets the deadline for future Read calls
Expand Down Expand Up @@ -420,7 +418,7 @@ func mapErr(err error) error {
case context.Canceled:
return errCanceled
case context.DeadlineExceeded:
return poll.ErrTimeout
return errTimeout
default:
return err
}
Expand Down Expand Up @@ -567,6 +565,21 @@ func (e InvalidAddrError) Error() string { return string(e) }
func (e InvalidAddrError) Timeout() bool { return false }
func (e InvalidAddrError) Temporary() bool { return false }

// errTimeout exists to return the historical "i/o timeout" string
// for context.DeadlineExceeded. See mapErr.
// It is also used when Dialer.Deadline is exceeded.
//
// TODO(iant): We could consider changing this to os.ErrDeadlineExceeded
// in the future, but note that that would conflict with the TODO
// at mapErr that suggests changing it to context.DeadlineExceeded.
var errTimeout error = &timeoutError{}

type timeoutError struct{}

func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }

// DNSConfigError represents an error reading the machine's DNS configuration.
// (No longer used; kept for compatibility.)
type DNSConfigError struct {
Expand Down
Loading

0 comments on commit d422f54

Please sign in to comment.