// Copyright 2015 CoreOS, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package discovery import ( "errors" "math" "math/rand" "net/http" "reflect" "sort" "strconv" "testing" "time" "github.com/coreos/etcd/Godeps/_workspace/src/github.com/jonboulle/clockwork" "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context" "github.com/coreos/etcd/client" ) const ( maxRetryInTest = 3 ) func TestNewProxyFuncUnset(t *testing.T) { pf, err := newProxyFunc("") if pf != nil { t.Fatal("unexpected non-nil proxyFunc") } if err != nil { t.Fatalf("unexpected non-nil err: %v", err) } } func TestNewProxyFuncBad(t *testing.T) { tests := []string{ "%%", "http://foo.com/%1", } for i, in := range tests { pf, err := newProxyFunc(in) if pf != nil { t.Errorf("#%d: unexpected non-nil proxyFunc", i) } if err == nil { t.Errorf("#%d: unexpected nil err", i) } } } func TestNewProxyFunc(t *testing.T) { tests := map[string]string{ "bar.com": "http://bar.com", "http://disco.foo.bar": "http://disco.foo.bar", } for in, w := range tests { pf, err := newProxyFunc(in) if pf == nil { t.Errorf("%s: unexpected nil proxyFunc", in) continue } if err != nil { t.Errorf("%s: unexpected non-nil err: %v", in, err) continue } g, err := pf(&http.Request{}) if err != nil { t.Errorf("%s: unexpected non-nil err: %v", in, err) } if g.String() != w { t.Errorf("%s: proxyURL=%q, want %q", in, g, w) } } } func TestCheckCluster(t *testing.T) { cluster := "/prefix/1000" self := "/1000/1" tests := []struct { nodes []*client.Node index uint64 werr error wsize int }{ { // self is in the size range []*client.Node{ {Key: "/1000/_config/size", Value: "3", CreatedIndex: 1}, {Key: "/1000/_config/"}, {Key: self, CreatedIndex: 2}, {Key: "/1000/2", CreatedIndex: 3}, {Key: "/1000/3", CreatedIndex: 4}, {Key: "/1000/4", CreatedIndex: 5}, }, 5, nil, 3, }, { // self is in the size range []*client.Node{ {Key: "/1000/_config/size", Value: "3", CreatedIndex: 1}, {Key: "/1000/_config/"}, {Key: "/1000/2", CreatedIndex: 2}, {Key: "/1000/3", CreatedIndex: 3}, {Key: self, CreatedIndex: 4}, {Key: "/1000/4", CreatedIndex: 5}, }, 5, nil, 3, }, { // self is out of the size range []*client.Node{ {Key: "/1000/_config/size", Value: "3", CreatedIndex: 1}, {Key: "/1000/_config/"}, {Key: "/1000/2", CreatedIndex: 2}, {Key: "/1000/3", CreatedIndex: 3}, {Key: "/1000/4", CreatedIndex: 4}, {Key: self, CreatedIndex: 5}, }, 5, ErrFullCluster, 3, }, { // self is not in the cluster []*client.Node{ {Key: "/1000/_config/size", Value: "3", CreatedIndex: 1}, {Key: "/1000/_config/"}, {Key: "/1000/2", CreatedIndex: 2}, {Key: "/1000/3", CreatedIndex: 3}, }, 3, nil, 3, }, { []*client.Node{ {Key: "/1000/_config/size", Value: "3", CreatedIndex: 1}, {Key: "/1000/_config/"}, {Key: "/1000/2", CreatedIndex: 2}, {Key: "/1000/3", CreatedIndex: 3}, {Key: "/1000/4", CreatedIndex: 4}, }, 3, ErrFullCluster, 3, }, { // bad size key []*client.Node{ {Key: "/1000/_config/size", Value: "bad", CreatedIndex: 1}, }, 0, ErrBadSizeKey, 0, }, { // no size key []*client.Node{}, 0, ErrSizeNotFound, 0, }, } for i, tt := range tests { rs := make([]*client.Response, 0) if len(tt.nodes) > 0 { rs = append(rs, &client.Response{Node: tt.nodes[0], Index: tt.index}) rs = append(rs, &client.Response{ Node: &client.Node{ Key: cluster, Nodes: tt.nodes[1:], }, Index: tt.index, }) } c := &clientWithResp{rs: rs} dBase := discovery{cluster: cluster, id: 1, c: c} cRetry := &clientWithRetry{failTimes: 3} cRetry.rs = rs fc := clockwork.NewFakeClock() dRetry := discovery{cluster: cluster, id: 1, c: cRetry, clock: fc} for _, d := range []discovery{dBase, dRetry} { go func() { for i := uint(1); i <= maxRetryInTest; i++ { fc.BlockUntil(1) fc.Advance(time.Second * (0x1 << i)) } }() ns, size, index, err := d.checkCluster() if err != tt.werr { t.Errorf("#%d: err = %v, want %v", i, err, tt.werr) } if reflect.DeepEqual(ns, tt.nodes) { t.Errorf("#%d: nodes = %v, want %v", i, ns, tt.nodes) } if size != tt.wsize { t.Errorf("#%d: size = %v, want %d", i, size, tt.wsize) } if index != tt.index { t.Errorf("#%d: index = %v, want %d", i, index, tt.index) } } } } func TestWaitNodes(t *testing.T) { all := client.Nodes{ 0: {Key: "/1000/1", CreatedIndex: 2}, 1: {Key: "/1000/2", CreatedIndex: 3}, 2: {Key: "/1000/3", CreatedIndex: 4}, } tests := []struct { nodes client.Nodes rs []*client.Response }{ { all, []*client.Response{}, }, { all[:1], []*client.Response{ {Node: &client.Node{Key: "/1000/2", CreatedIndex: 3}}, {Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}}, }, }, { all[:2], []*client.Response{ {Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}}, }, }, { append(all, &client.Node{Key: "/1000/4", CreatedIndex: 5}), []*client.Response{ {Node: &client.Node{Key: "/1000/3", CreatedIndex: 4}}, }, }, } for i, tt := range tests { // Basic case c := &clientWithResp{nil, &watcherWithResp{tt.rs}} dBase := &discovery{cluster: "1000", c: c} // Retry case retryScanResp := make([]*client.Response, 0) if len(tt.nodes) > 0 { retryScanResp = append(retryScanResp, &client.Response{ Node: &client.Node{ Key: "1000", Value: strconv.Itoa(3), }, }) retryScanResp = append(retryScanResp, &client.Response{ Node: &client.Node{ Nodes: tt.nodes, }, }) } cRetry := &clientWithResp{ rs: retryScanResp, w: &watcherWithRetry{rs: tt.rs, failTimes: 2}, } fc := clockwork.NewFakeClock() dRetry := &discovery{ cluster: "1000", c: cRetry, clock: fc, } for _, d := range []*discovery{dBase, dRetry} { go func() { for i := uint(1); i <= maxRetryInTest; i++ { fc.BlockUntil(1) fc.Advance(time.Second * (0x1 << i)) } }() g, err := d.waitNodes(tt.nodes, 3, 0) // we do not care about index in this test if err != nil { t.Errorf("#%d: err = %v, want %v", i, err, nil) } if !reflect.DeepEqual(g, all) { t.Errorf("#%d: all = %v, want %v", i, g, all) } } } } func TestCreateSelf(t *testing.T) { rs := []*client.Response{{Node: &client.Node{Key: "1000/1", CreatedIndex: 2}}} w := &watcherWithResp{rs} errw := &watcherWithErr{errors.New("watch err")} c := &clientWithResp{rs, w} errc := &clientWithErr{errors.New("create err"), w} errwc := &clientWithResp{rs, errw} tests := []struct { c client.KeysAPI werr error }{ // no error {c, nil}, // client.create returns an error {errc, errc.err}, // watcher.next retuens an error {errwc, errw.err}, } for i, tt := range tests { d := discovery{cluster: "1000", c: tt.c} if err := d.createSelf(""); err != tt.werr { t.Errorf("#%d: err = %v, want %v", i, err, nil) } } } func TestNodesToCluster(t *testing.T) { nodes := client.Nodes{ 0: {Key: "/1000/1", Value: "1=1.1.1.1", CreatedIndex: 1}, 1: {Key: "/1000/2", Value: "2=2.2.2.2", CreatedIndex: 2}, 2: {Key: "/1000/3", Value: "3=3.3.3.3", CreatedIndex: 3}, } w := "1=1.1.1.1,2=2.2.2.2,3=3.3.3.3" cluster := nodesToCluster(nodes) if !reflect.DeepEqual(cluster, w) { t.Errorf("cluster = %v, want %v", cluster, w) } } func TestSortableNodes(t *testing.T) { ns := client.Nodes{ 0: {CreatedIndex: 5}, 1: {CreatedIndex: 1}, 2: {CreatedIndex: 3}, 3: {CreatedIndex: 4}, } // add some randomness for i := 0; i < 10000; i++ { ns = append(ns, &client.Node{CreatedIndex: uint64(rand.Int31())}) } sns := sortableNodes{ns} sort.Sort(sns) cis := make([]int, 0) for _, n := range sns.Nodes { cis = append(cis, int(n.CreatedIndex)) } if sort.IntsAreSorted(cis) != true { t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true) } cis = make([]int, 0) for _, n := range ns { cis = append(cis, int(n.CreatedIndex)) } if sort.IntsAreSorted(cis) != true { t.Errorf("isSorted = %v, want %v", sort.IntsAreSorted(cis), true) } } func TestRetryFailure(t *testing.T) { nRetries = maxRetryInTest defer func() { nRetries = math.MaxUint32 }() cluster := "1000" c := &clientWithRetry{failTimes: 4} fc := clockwork.NewFakeClock() d := discovery{ cluster: cluster, id: 1, c: c, clock: fc, } go func() { for i := uint(1); i <= maxRetryInTest; i++ { fc.BlockUntil(1) fc.Advance(time.Second * (0x1 << i)) } }() if _, _, _, err := d.checkCluster(); err != ErrTooManyRetries { t.Errorf("err = %v, want %v", err, ErrTooManyRetries) } } type clientWithResp struct { rs []*client.Response w client.Watcher } func (c *clientWithResp) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) { if len(c.rs) == 0 { return &client.Response{}, nil } r := c.rs[0] c.rs = c.rs[1:] return r, nil } func (c *clientWithResp) Get(ctx context.Context, key string) (*client.Response, error) { if len(c.rs) == 0 { return &client.Response{}, client.ErrKeyNoExist } r := c.rs[0] c.rs = append(c.rs[1:], r) return r, nil } func (c *clientWithResp) Watch(key string, waitIndex uint64) client.Watcher { return c.w } func (c *clientWithResp) RecursiveWatch(key string, waitIndex uint64) client.Watcher { return c.w } type clientWithErr struct { err error w client.Watcher } func (c *clientWithErr) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) { return &client.Response{}, c.err } func (c *clientWithErr) Get(ctx context.Context, key string) (*client.Response, error) { return &client.Response{}, c.err } func (c *clientWithErr) Watch(key string, waitIndex uint64) client.Watcher { return c.w } func (c *clientWithErr) RecursiveWatch(key string, waitIndex uint64) client.Watcher { return c.w } type watcherWithResp struct { rs []*client.Response } func (w *watcherWithResp) Next(context.Context) (*client.Response, error) { if len(w.rs) == 0 { return &client.Response{}, nil } r := w.rs[0] w.rs = w.rs[1:] return r, nil } type watcherWithErr struct { err error } func (w *watcherWithErr) Next(context.Context) (*client.Response, error) { return &client.Response{}, w.err } // clientWithRetry will timeout all requests up to failTimes type clientWithRetry struct { clientWithResp failCount int failTimes int } func (c *clientWithRetry) Create(ctx context.Context, key string, value string, ttl time.Duration) (*client.Response, error) { if c.failCount < c.failTimes { c.failCount++ return nil, client.ErrTimeout } return c.clientWithResp.Create(ctx, key, value, ttl) } func (c *clientWithRetry) Get(ctx context.Context, key string) (*client.Response, error) { if c.failCount < c.failTimes { c.failCount++ return nil, client.ErrTimeout } return c.clientWithResp.Get(ctx, key) } // watcherWithRetry will timeout all requests up to failTimes type watcherWithRetry struct { rs []*client.Response failCount int failTimes int } func (w *watcherWithRetry) Next(context.Context) (*client.Response, error) { if w.failCount < w.failTimes { w.failCount++ return nil, client.ErrTimeout } if len(w.rs) == 0 { return &client.Response{}, nil } r := w.rs[0] w.rs = w.rs[1:] return r, nil }