Why doesn't this select statement timeout?

210 views
Skip to first unread message

gopher...@gmail.com

unread,
May 22, 2018, 12:50:16 AM5/22/18
to golang-nuts
The following setup uses a select statement to either wait for a response error or timeout after a given duration.

The httptest.Server is setup to force the timeout by sleeping (1 sec) for greater than the timeout (1 millisec).

But the timeout case isn't being hit at all, why? what is wrong with the setup?


package main

import (
"fmt"
"time"
"net/http"
"net/http/httptest"
"errors"
)

func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(1 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)
f := func(r *http.Request) error {
_, err := http.DefaultClient.Do(r)
return err
}
ch := make(chan error, 1)
timeout := 1 * time.Millisecond
select {
case ch <-f(req):
case <-time.After(timeout):
ch <- errors.New("timeout")
}
err:= <-ch
close(ch)
fmt.Println(err) // outputs <nil>, not timeout
// Output:
// timeout
}


Burak Serdar

unread,
May 22, 2018, 1:25:08 AM5/22/18
to gopher...@gmail.com, golang-nuts
On Mon, May 21, 2018 at 12:13 PM, <gopher...@gmail.com> wrote:
> timeout := 1 * time.Millisecond
> select {
> case ch <-f(req):
> case <-time.After(timeout):
> ch <- errors.New("timeout")
> }

The instant 'select' is evaluated, ch is writable. Also time.After()
is called, and returns a channel that will be ready to be read in a
millisecond, i.e. not readable immediately. So it'll write to ch, and
will never timeout.

To do what you described, you need to run the request piece in a
goroutine, and write the result to a channel:

go func() {
_,err:=http.DefaultClient.Do(...)
ch<-err
}()

Then, wait for the error to arrive on ch, or timeout:

select {
case err:=<-ch:
case <-time.After(timeout):
}


> err:= <-ch
> close(ch)
> fmt.Println(err) // outputs <nil>, not timeout
> // Output:
> // timeout
> }
>
>
> --
> You received this message because you are subscribed to the Google Groups
> "golang-nuts" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to golang-nuts...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

ag

unread,
May 22, 2018, 7:29:59 PM5/22/18
to golang-nuts
This is interesting and I still don't get why it's not working.

I modified the code a bit and expected it to immediately break out of select but it still waits for http calls to finish


package main

import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
)

func main() {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(1 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

req, _ := http.NewRequest(http.MethodGet, ts.URL, nil)

f := func(r *http.Request, n int) error {
fmt.Println("Making the http call ", n)
_, err := http.DefaultClient.Do(r)
return err
}

ch := make(chan error, 1)

timeout := 1 * time.Millisecond
select {
case ch <- errors.New("immediate timeout"):
case ch <- f(req, 1):
case ch <- f(req, 2):
case <-time.After(timeout):
fmt.Println("Timeout being called")
ch <- errors.New("timeout")
}

err1 := <-ch
fmt.Println("Received first error", err1)
close(ch)


// Output:
       // Making the http call  1
       // Making the http call  2
       // Received first error immediate timeout
}



alex....@gmail.com

unread,
May 22, 2018, 8:29:43 PM5/22/18
to golang-nuts
https://golang.org/ref/spec#Select_statements

Execution of a "select" statement proceeds in several steps:

  1. For all the cases in the statement, the channel operands of receive operations and the channel and right-hand-side expressions of send statements are evaluated exactly once, in source order, upon entering the "select" statement. The result is a set of channels to receive from or send to, and the corresponding values to send. Any side effects in that evaluation will occur irrespective of which (if any) communication operation is selected to proceed. Expressions on the left-hand side of a RecvStmt with a short variable declaration or assignment are not yet evaluated.
  2. If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.
  3. Unless the selected case is the default case, the respective communication operation is executed.
  4. If the selected case is a RecvStmt with a short variable declaration or an assignment, the left-hand side expressions are evaluated and the received value (or values) are assigned.
  5. The statement list of the selected case is executed.

So it evaluates all your function calls before doing the select in the order they appear in single threadeddly. 

ag

unread,
May 22, 2018, 9:00:02 PM5/22/18
to golang-nuts
That clarifies it. Thanks!

gopher...@gmail.com

unread,
May 24, 2018, 4:58:20 AM5/24/18
to golang-nuts
Thanks for the reply.

So you're saying that all case statements are evaluated first before a choice is made.
Which means that the full request/response must complete even if the timeout case finishes first.

If I swap the cases, now I consistently get a `timeout`. 
All cases are run to completion, so the full request/response occurs.
Whats the reason for selecting the timeout? shouldn't it be pseudo-random?
https://play.golang.org/p/M-7rMPJxOWq

How should the done channel be setup to get this example working properly?



On Tuesday, May 22, 2018 at 5:29:43 PM UTC-7, alex....@gmail.com wrote:

alex....@gmail.com

unread,
May 24, 2018, 11:26:36 AM5/24/18
to golang-nuts
No, not the entire case statements, only the statements on the right of the "<-"

btw if you run it out of the playground (which makes concurrency more deterministic)
You would get timeout sometimes and nil sometimes

Also maybe this example would help you understand what's going on.

gopher...@gmail.com

unread,
May 25, 2018, 4:12:54 AM5/25/18
to golang-nuts
That is an excellent example. Thanks!
Reply all
Reply to author
Forward
0 new messages