
Change the compiler to use the new routines. Drop the separation of small and large values when sending on a channel. Allocate the select struct on the stack. Remove the old C implementation of channels. Adjust the garbage collector for the new data structure. Bring in part of the tracing code, enough for the channel code to call. Bump the permitted number of allocations in one of the tests in context_test.go. The difference is that now receiving from a channel allocates a sudog, which the C code used to simply put on the stack. This will be somewhat better when we port proc.go. Reviewed-on: https://go-review.googlesource.com/30714 From-SVN: r240941
614 lines
16 KiB
Go
614 lines
16 KiB
Go
// Copyright 2014 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package context
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// otherContext is a Context that's not one of the types defined in context.go.
|
|
// This lets us test code paths that differ based on the underlying type of the
|
|
// Context.
|
|
type otherContext struct {
|
|
Context
|
|
}
|
|
|
|
func TestBackground(t *testing.T) {
|
|
c := Background()
|
|
if c == nil {
|
|
t.Fatalf("Background returned nil")
|
|
}
|
|
select {
|
|
case x := <-c.Done():
|
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
if got, want := fmt.Sprint(c), "context.Background"; got != want {
|
|
t.Errorf("Background().String() = %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestTODO(t *testing.T) {
|
|
c := TODO()
|
|
if c == nil {
|
|
t.Fatalf("TODO returned nil")
|
|
}
|
|
select {
|
|
case x := <-c.Done():
|
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
if got, want := fmt.Sprint(c), "context.TODO"; got != want {
|
|
t.Errorf("TODO().String() = %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestWithCancel(t *testing.T) {
|
|
c1, cancel := WithCancel(Background())
|
|
|
|
if got, want := fmt.Sprint(c1), "context.Background.WithCancel"; got != want {
|
|
t.Errorf("c1.String() = %q want %q", got, want)
|
|
}
|
|
|
|
o := otherContext{c1}
|
|
c2, _ := WithCancel(o)
|
|
contexts := []Context{c1, o, c2}
|
|
|
|
for i, c := range contexts {
|
|
if d := c.Done(); d == nil {
|
|
t.Errorf("c[%d].Done() == %v want non-nil", i, d)
|
|
}
|
|
if e := c.Err(); e != nil {
|
|
t.Errorf("c[%d].Err() == %v want nil", i, e)
|
|
}
|
|
|
|
select {
|
|
case x := <-c.Done():
|
|
t.Errorf("<-c.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
}
|
|
|
|
cancel()
|
|
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
|
|
|
for i, c := range contexts {
|
|
select {
|
|
case <-c.Done():
|
|
default:
|
|
t.Errorf("<-c[%d].Done() blocked, but shouldn't have", i)
|
|
}
|
|
if e := c.Err(); e != Canceled {
|
|
t.Errorf("c[%d].Err() == %v want %v", i, e, Canceled)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParentFinishesChild(t *testing.T) {
|
|
// Context tree:
|
|
// parent -> cancelChild
|
|
// parent -> valueChild -> timerChild
|
|
parent, cancel := WithCancel(Background())
|
|
cancelChild, stop := WithCancel(parent)
|
|
defer stop()
|
|
valueChild := WithValue(parent, "key", "value")
|
|
timerChild, stop := WithTimeout(valueChild, 10000*time.Hour)
|
|
defer stop()
|
|
|
|
select {
|
|
case x := <-parent.Done():
|
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
case x := <-cancelChild.Done():
|
|
t.Errorf("<-cancelChild.Done() == %v want nothing (it should block)", x)
|
|
case x := <-timerChild.Done():
|
|
t.Errorf("<-timerChild.Done() == %v want nothing (it should block)", x)
|
|
case x := <-valueChild.Done():
|
|
t.Errorf("<-valueChild.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
|
|
// The parent's children should contain the two cancelable children.
|
|
pc := parent.(*cancelCtx)
|
|
cc := cancelChild.(*cancelCtx)
|
|
tc := timerChild.(*timerCtx)
|
|
pc.mu.Lock()
|
|
if len(pc.children) != 2 || !pc.children[cc] || !pc.children[tc] {
|
|
t.Errorf("bad linkage: pc.children = %v, want %v and %v",
|
|
pc.children, cc, tc)
|
|
}
|
|
pc.mu.Unlock()
|
|
|
|
if p, ok := parentCancelCtx(cc.Context); !ok || p != pc {
|
|
t.Errorf("bad linkage: parentCancelCtx(cancelChild.Context) = %v, %v want %v, true", p, ok, pc)
|
|
}
|
|
if p, ok := parentCancelCtx(tc.Context); !ok || p != pc {
|
|
t.Errorf("bad linkage: parentCancelCtx(timerChild.Context) = %v, %v want %v, true", p, ok, pc)
|
|
}
|
|
|
|
cancel()
|
|
|
|
pc.mu.Lock()
|
|
if len(pc.children) != 0 {
|
|
t.Errorf("pc.cancel didn't clear pc.children = %v", pc.children)
|
|
}
|
|
pc.mu.Unlock()
|
|
|
|
// parent and children should all be finished.
|
|
check := func(ctx Context, name string) {
|
|
select {
|
|
case <-ctx.Done():
|
|
default:
|
|
t.Errorf("<-%s.Done() blocked, but shouldn't have", name)
|
|
}
|
|
if e := ctx.Err(); e != Canceled {
|
|
t.Errorf("%s.Err() == %v want %v", name, e, Canceled)
|
|
}
|
|
}
|
|
check(parent, "parent")
|
|
check(cancelChild, "cancelChild")
|
|
check(valueChild, "valueChild")
|
|
check(timerChild, "timerChild")
|
|
|
|
// WithCancel should return a canceled context on a canceled parent.
|
|
precanceledChild := WithValue(parent, "key", "value")
|
|
select {
|
|
case <-precanceledChild.Done():
|
|
default:
|
|
t.Errorf("<-precanceledChild.Done() blocked, but shouldn't have")
|
|
}
|
|
if e := precanceledChild.Err(); e != Canceled {
|
|
t.Errorf("precanceledChild.Err() == %v want %v", e, Canceled)
|
|
}
|
|
}
|
|
|
|
func TestChildFinishesFirst(t *testing.T) {
|
|
cancelable, stop := WithCancel(Background())
|
|
defer stop()
|
|
for _, parent := range []Context{Background(), cancelable} {
|
|
child, cancel := WithCancel(parent)
|
|
|
|
select {
|
|
case x := <-parent.Done():
|
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
case x := <-child.Done():
|
|
t.Errorf("<-child.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
|
|
cc := child.(*cancelCtx)
|
|
pc, pcok := parent.(*cancelCtx) // pcok == false when parent == Background()
|
|
if p, ok := parentCancelCtx(cc.Context); ok != pcok || (ok && pc != p) {
|
|
t.Errorf("bad linkage: parentCancelCtx(cc.Context) = %v, %v want %v, %v", p, ok, pc, pcok)
|
|
}
|
|
|
|
if pcok {
|
|
pc.mu.Lock()
|
|
if len(pc.children) != 1 || !pc.children[cc] {
|
|
t.Errorf("bad linkage: pc.children = %v, cc = %v", pc.children, cc)
|
|
}
|
|
pc.mu.Unlock()
|
|
}
|
|
|
|
cancel()
|
|
|
|
if pcok {
|
|
pc.mu.Lock()
|
|
if len(pc.children) != 0 {
|
|
t.Errorf("child's cancel didn't remove self from pc.children = %v", pc.children)
|
|
}
|
|
pc.mu.Unlock()
|
|
}
|
|
|
|
// child should be finished.
|
|
select {
|
|
case <-child.Done():
|
|
default:
|
|
t.Errorf("<-child.Done() blocked, but shouldn't have")
|
|
}
|
|
if e := child.Err(); e != Canceled {
|
|
t.Errorf("child.Err() == %v want %v", e, Canceled)
|
|
}
|
|
|
|
// parent should not be finished.
|
|
select {
|
|
case x := <-parent.Done():
|
|
t.Errorf("<-parent.Done() == %v want nothing (it should block)", x)
|
|
default:
|
|
}
|
|
if e := parent.Err(); e != nil {
|
|
t.Errorf("parent.Err() == %v want nil", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testDeadline(c Context, name string, failAfter time.Duration, t *testing.T) {
|
|
select {
|
|
case <-time.After(failAfter):
|
|
t.Fatalf("%s: context should have timed out", name)
|
|
case <-c.Done():
|
|
}
|
|
if e := c.Err(); e != DeadlineExceeded {
|
|
t.Errorf("%s: c.Err() == %v; want %v", name, e, DeadlineExceeded)
|
|
}
|
|
}
|
|
|
|
func TestDeadline(t *testing.T) {
|
|
c, _ := WithDeadline(Background(), time.Now().Add(50*time.Millisecond))
|
|
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
|
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
|
}
|
|
testDeadline(c, "WithDeadline", time.Second, t)
|
|
|
|
c, _ = WithDeadline(Background(), time.Now().Add(50*time.Millisecond))
|
|
o := otherContext{c}
|
|
testDeadline(o, "WithDeadline+otherContext", time.Second, t)
|
|
|
|
c, _ = WithDeadline(Background(), time.Now().Add(50*time.Millisecond))
|
|
o = otherContext{c}
|
|
c, _ = WithDeadline(o, time.Now().Add(4*time.Second))
|
|
testDeadline(c, "WithDeadline+otherContext+WithDeadline", 2*time.Second, t)
|
|
|
|
c, _ = WithDeadline(Background(), time.Now().Add(-time.Millisecond))
|
|
testDeadline(c, "WithDeadline+inthepast", time.Second, t)
|
|
|
|
c, _ = WithDeadline(Background(), time.Now())
|
|
testDeadline(c, "WithDeadline+now", time.Second, t)
|
|
}
|
|
|
|
func TestTimeout(t *testing.T) {
|
|
c, _ := WithTimeout(Background(), 50*time.Millisecond)
|
|
if got, prefix := fmt.Sprint(c), "context.Background.WithDeadline("; !strings.HasPrefix(got, prefix) {
|
|
t.Errorf("c.String() = %q want prefix %q", got, prefix)
|
|
}
|
|
testDeadline(c, "WithTimeout", time.Second, t)
|
|
|
|
c, _ = WithTimeout(Background(), 50*time.Millisecond)
|
|
o := otherContext{c}
|
|
testDeadline(o, "WithTimeout+otherContext", time.Second, t)
|
|
|
|
c, _ = WithTimeout(Background(), 50*time.Millisecond)
|
|
o = otherContext{c}
|
|
c, _ = WithTimeout(o, 3*time.Second)
|
|
testDeadline(c, "WithTimeout+otherContext+WithTimeout", 2*time.Second, t)
|
|
}
|
|
|
|
func TestCanceledTimeout(t *testing.T) {
|
|
c, _ := WithTimeout(Background(), time.Second)
|
|
o := otherContext{c}
|
|
c, cancel := WithTimeout(o, 2*time.Second)
|
|
cancel()
|
|
time.Sleep(100 * time.Millisecond) // let cancelation propagate
|
|
select {
|
|
case <-c.Done():
|
|
default:
|
|
t.Errorf("<-c.Done() blocked, but shouldn't have")
|
|
}
|
|
if e := c.Err(); e != Canceled {
|
|
t.Errorf("c.Err() == %v want %v", e, Canceled)
|
|
}
|
|
}
|
|
|
|
type key1 int
|
|
type key2 int
|
|
|
|
var k1 = key1(1)
|
|
var k2 = key2(1) // same int as k1, different type
|
|
var k3 = key2(3) // same type as k2, different int
|
|
|
|
func TestValues(t *testing.T) {
|
|
check := func(c Context, nm, v1, v2, v3 string) {
|
|
if v, ok := c.Value(k1).(string); ok == (len(v1) == 0) || v != v1 {
|
|
t.Errorf(`%s.Value(k1).(string) = %q, %t want %q, %t`, nm, v, ok, v1, len(v1) != 0)
|
|
}
|
|
if v, ok := c.Value(k2).(string); ok == (len(v2) == 0) || v != v2 {
|
|
t.Errorf(`%s.Value(k2).(string) = %q, %t want %q, %t`, nm, v, ok, v2, len(v2) != 0)
|
|
}
|
|
if v, ok := c.Value(k3).(string); ok == (len(v3) == 0) || v != v3 {
|
|
t.Errorf(`%s.Value(k3).(string) = %q, %t want %q, %t`, nm, v, ok, v3, len(v3) != 0)
|
|
}
|
|
}
|
|
|
|
c0 := Background()
|
|
check(c0, "c0", "", "", "")
|
|
|
|
c1 := WithValue(Background(), k1, "c1k1")
|
|
check(c1, "c1", "c1k1", "", "")
|
|
|
|
if got, want := fmt.Sprint(c1), `context.Background.WithValue(1, "c1k1")`; got != want {
|
|
t.Errorf("c.String() = %q want %q", got, want)
|
|
}
|
|
|
|
c2 := WithValue(c1, k2, "c2k2")
|
|
check(c2, "c2", "c1k1", "c2k2", "")
|
|
|
|
c3 := WithValue(c2, k3, "c3k3")
|
|
check(c3, "c2", "c1k1", "c2k2", "c3k3")
|
|
|
|
c4 := WithValue(c3, k1, nil)
|
|
check(c4, "c4", "", "c2k2", "c3k3")
|
|
|
|
o0 := otherContext{Background()}
|
|
check(o0, "o0", "", "", "")
|
|
|
|
o1 := otherContext{WithValue(Background(), k1, "c1k1")}
|
|
check(o1, "o1", "c1k1", "", "")
|
|
|
|
o2 := WithValue(o1, k2, "o2k2")
|
|
check(o2, "o2", "c1k1", "o2k2", "")
|
|
|
|
o3 := otherContext{c4}
|
|
check(o3, "o3", "", "c2k2", "c3k3")
|
|
|
|
o4 := WithValue(o3, k3, nil)
|
|
check(o4, "o4", "", "c2k2", "")
|
|
}
|
|
|
|
func TestAllocs(t *testing.T) {
|
|
bg := Background()
|
|
for _, test := range []struct {
|
|
desc string
|
|
f func()
|
|
limit float64
|
|
gccgoLimit float64
|
|
}{
|
|
{
|
|
desc: "Background()",
|
|
f: func() { Background() },
|
|
limit: 0,
|
|
gccgoLimit: 0,
|
|
},
|
|
{
|
|
desc: fmt.Sprintf("WithValue(bg, %v, nil)", k1),
|
|
f: func() {
|
|
c := WithValue(bg, k1, nil)
|
|
c.Value(k1)
|
|
},
|
|
limit: 3,
|
|
gccgoLimit: 3,
|
|
},
|
|
{
|
|
desc: "WithTimeout(bg, 15*time.Millisecond)",
|
|
f: func() {
|
|
c, _ := WithTimeout(bg, 15*time.Millisecond)
|
|
<-c.Done()
|
|
},
|
|
limit: 8,
|
|
gccgoLimit: 18,
|
|
},
|
|
{
|
|
desc: "WithCancel(bg)",
|
|
f: func() {
|
|
c, cancel := WithCancel(bg)
|
|
cancel()
|
|
<-c.Done()
|
|
},
|
|
limit: 5,
|
|
gccgoLimit: 8,
|
|
},
|
|
{
|
|
desc: "WithTimeout(bg, 5*time.Millisecond)",
|
|
f: func() {
|
|
c, cancel := WithTimeout(bg, 5*time.Millisecond)
|
|
cancel()
|
|
<-c.Done()
|
|
},
|
|
limit: 8,
|
|
gccgoLimit: 25,
|
|
},
|
|
} {
|
|
limit := test.limit
|
|
if runtime.Compiler == "gccgo" {
|
|
// gccgo does not yet do escape analysis.
|
|
// TOOD(iant): Remove this when gccgo does do escape analysis.
|
|
limit = test.gccgoLimit
|
|
}
|
|
numRuns := 100
|
|
if testing.Short() {
|
|
numRuns = 10
|
|
}
|
|
if n := testing.AllocsPerRun(numRuns, test.f); n > limit {
|
|
t.Errorf("%s allocs = %f want %d", test.desc, n, int(limit))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSimultaneousCancels(t *testing.T) {
|
|
root, cancel := WithCancel(Background())
|
|
m := map[Context]CancelFunc{root: cancel}
|
|
q := []Context{root}
|
|
// Create a tree of contexts.
|
|
for len(q) != 0 && len(m) < 100 {
|
|
parent := q[0]
|
|
q = q[1:]
|
|
for i := 0; i < 4; i++ {
|
|
ctx, cancel := WithCancel(parent)
|
|
m[ctx] = cancel
|
|
q = append(q, ctx)
|
|
}
|
|
}
|
|
// Start all the cancels in a random order.
|
|
var wg sync.WaitGroup
|
|
wg.Add(len(m))
|
|
for _, cancel := range m {
|
|
go func(cancel CancelFunc) {
|
|
cancel()
|
|
wg.Done()
|
|
}(cancel)
|
|
}
|
|
// Wait on all the contexts in a random order.
|
|
for ctx := range m {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(1 * time.Second):
|
|
buf := make([]byte, 10<<10)
|
|
n := runtime.Stack(buf, true)
|
|
t.Fatalf("timed out waiting for <-ctx.Done(); stacks:\n%s", buf[:n])
|
|
}
|
|
}
|
|
// Wait for all the cancel functions to return.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(1 * time.Second):
|
|
buf := make([]byte, 10<<10)
|
|
n := runtime.Stack(buf, true)
|
|
t.Fatalf("timed out waiting for cancel functions; stacks:\n%s", buf[:n])
|
|
}
|
|
}
|
|
|
|
func TestInterlockedCancels(t *testing.T) {
|
|
parent, cancelParent := WithCancel(Background())
|
|
child, cancelChild := WithCancel(parent)
|
|
go func() {
|
|
parent.Done()
|
|
cancelChild()
|
|
}()
|
|
cancelParent()
|
|
select {
|
|
case <-child.Done():
|
|
case <-time.After(1 * time.Second):
|
|
buf := make([]byte, 10<<10)
|
|
n := runtime.Stack(buf, true)
|
|
t.Fatalf("timed out waiting for child.Done(); stacks:\n%s", buf[:n])
|
|
}
|
|
}
|
|
|
|
func TestLayersCancel(t *testing.T) {
|
|
testLayers(t, time.Now().UnixNano(), false)
|
|
}
|
|
|
|
func TestLayersTimeout(t *testing.T) {
|
|
testLayers(t, time.Now().UnixNano(), true)
|
|
}
|
|
|
|
func testLayers(t *testing.T, seed int64, testTimeout bool) {
|
|
rand.Seed(seed)
|
|
errorf := func(format string, a ...interface{}) {
|
|
t.Errorf(fmt.Sprintf("seed=%d: %s", seed, format), a...)
|
|
}
|
|
const (
|
|
timeout = 200 * time.Millisecond
|
|
minLayers = 30
|
|
)
|
|
type value int
|
|
var (
|
|
vals []*value
|
|
cancels []CancelFunc
|
|
numTimers int
|
|
ctx = Background()
|
|
)
|
|
for i := 0; i < minLayers || numTimers == 0 || len(cancels) == 0 || len(vals) == 0; i++ {
|
|
switch rand.Intn(3) {
|
|
case 0:
|
|
v := new(value)
|
|
ctx = WithValue(ctx, v, v)
|
|
vals = append(vals, v)
|
|
case 1:
|
|
var cancel CancelFunc
|
|
ctx, cancel = WithCancel(ctx)
|
|
cancels = append(cancels, cancel)
|
|
case 2:
|
|
var cancel CancelFunc
|
|
ctx, cancel = WithTimeout(ctx, timeout)
|
|
cancels = append(cancels, cancel)
|
|
numTimers++
|
|
}
|
|
}
|
|
checkValues := func(when string) {
|
|
for _, key := range vals {
|
|
if val := ctx.Value(key).(*value); key != val {
|
|
errorf("%s: ctx.Value(%p) = %p want %p", when, key, val, key)
|
|
}
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
errorf("ctx should not be canceled yet")
|
|
default:
|
|
}
|
|
if s, prefix := fmt.Sprint(ctx), "context.Background."; !strings.HasPrefix(s, prefix) {
|
|
t.Errorf("ctx.String() = %q want prefix %q", s, prefix)
|
|
}
|
|
t.Log(ctx)
|
|
checkValues("before cancel")
|
|
if testTimeout {
|
|
select {
|
|
case <-ctx.Done():
|
|
case <-time.After(timeout + time.Second):
|
|
errorf("ctx should have timed out")
|
|
}
|
|
checkValues("after timeout")
|
|
} else {
|
|
cancel := cancels[rand.Intn(len(cancels))]
|
|
cancel()
|
|
select {
|
|
case <-ctx.Done():
|
|
default:
|
|
errorf("ctx should be canceled")
|
|
}
|
|
checkValues("after cancel")
|
|
}
|
|
}
|
|
|
|
func TestCancelRemoves(t *testing.T) {
|
|
checkChildren := func(when string, ctx Context, want int) {
|
|
if got := len(ctx.(*cancelCtx).children); got != want {
|
|
t.Errorf("%s: context has %d children, want %d", when, got, want)
|
|
}
|
|
}
|
|
|
|
ctx, _ := WithCancel(Background())
|
|
checkChildren("after creation", ctx, 0)
|
|
_, cancel := WithCancel(ctx)
|
|
checkChildren("with WithCancel child ", ctx, 1)
|
|
cancel()
|
|
checkChildren("after cancelling WithCancel child", ctx, 0)
|
|
|
|
ctx, _ = WithCancel(Background())
|
|
checkChildren("after creation", ctx, 0)
|
|
_, cancel = WithTimeout(ctx, 60*time.Minute)
|
|
checkChildren("with WithTimeout child ", ctx, 1)
|
|
cancel()
|
|
checkChildren("after cancelling WithTimeout child", ctx, 0)
|
|
}
|
|
|
|
func TestWithValueChecksKey(t *testing.T) {
|
|
panicVal := recoveredValue(func() { WithValue(Background(), []byte("foo"), "bar") })
|
|
if panicVal == nil {
|
|
t.Error("expected panic")
|
|
}
|
|
panicVal = recoveredValue(func() { WithValue(Background(), nil, "bar") })
|
|
if got, want := fmt.Sprint(panicVal), "nil key"; got != want {
|
|
t.Errorf("panic = %q; want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func recoveredValue(fn func()) (v interface{}) {
|
|
defer func() { v = recover() }()
|
|
fn()
|
|
return
|
|
}
|
|
|
|
func TestDeadlineExceededSupportsTimeout(t *testing.T) {
|
|
i, ok := DeadlineExceeded.(interface {
|
|
Timeout() bool
|
|
})
|
|
if !ok {
|
|
t.Fatal("DeadlineExceeded does not support Timeout interface")
|
|
}
|
|
if !i.Timeout() {
|
|
t.Fatal("wrong value for timeout")
|
|
}
|
|
}
|