blob: 51b0c09e9546bce72711fd8bb6ec7be96e39beff [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Note: We disable durEq and pingResultsEq when the race detector is enabled;
// they rely on timings that are thrown off by the race detector, which may
// increase execution time by 2-20x. We also disable these checks when running
// in Jenkins, because Jenkins machines are sometimes exceptionally slow.
package ping_test
import (
"errors"
"os"
"reflect"
"runtime/debug"
"strconv"
"testing"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/rpc"
"v.io/v23/verror"
_ "v.io/x/ref/runtime/factories/generic"
"v.io/x/ref/services/syncbase/ping"
"v.io/x/ref/services/syncbase/server/interfaces"
"v.io/x/ref/test"
)
////////////////////////////////////////////////////////////////////////////////
// Generic helpers
func fatal(t *testing.T, args ...interface{}) {
debug.PrintStack()
t.Fatal(args...)
}
func fatalf(t *testing.T, format string, args ...interface{}) {
debug.PrintStack()
t.Fatalf(format, args...)
}
func eq(t *testing.T, got, want interface{}) {
if !reflect.DeepEqual(got, want) {
fatalf(t, "got %v, want %v", got, want)
}
}
// TODO(jingjin): Detects whether this is Jenkins. Modeled after isCI function
// in v.io/x/devtools/jiri-test/internal/test/predicates.go. We should put this
// in some public location.
var isCI = os.Getenv("USER") == "veyron" || os.Getenv("V23_FORCE_CI") == "yes"
////////////////////////////////////////////////////////////////////////////////
// TestPingInParallel
type pingable struct {
delay time.Duration // how long to wait before replying to a ping
}
func (p *pingable) Ping(_ *context.T, _ rpc.ServerCall) error {
time.Sleep(p.delay)
return nil
}
// startPingableServer starts a server that can handle Ping RPCs. It returns the
// server's name (endpoint) and a cleanup func.
func startPingableServer(t *testing.T, ctx *context.T, delay time.Duration) (string, func()) {
service := interfaces.PingableServer(&pingable{delay: delay})
ctx, server, err := v23.WithNewServer(ctx, "", service, nil)
if err != nil {
fatal(t, err)
}
return server.Status().Endpoints[0].Name(), func() { server.Stop() }
}
func ms(d time.Duration) time.Duration {
return d * time.Millisecond
}
func abs(d time.Duration) time.Duration {
if d < 0 {
d = -d
}
return d
}
// durEq checks that the given durations are equal, with a bit of slack.
func durEq(t *testing.T, got, want time.Duration) {
if isCI || raceDetectorEnabled {
return // See comment at the top of this file.
}
// TODO(sadovsky): Reduce the slack once Vanadium implements a fast path for
// local RPCs.
if abs(got-want) > ms(15) { // 15ms slack
eq(t, got, want)
}
}
// pingResultsEq checks that the given ping results are equal, with a bit of
// slack around expected durations. Errors are considered equal if their verror
// IDs are equal.
func pingResultsEq(t *testing.T, got, want map[string]ping.PingResult) {
if isCI || raceDetectorEnabled {
return // See comment at the top of this file.
}
if len(got) != len(want) {
eq(t, got, want)
}
for name, gotRes := range got {
wantRes, ok := want[name]
if !ok {
eq(t, got, want)
}
eq(t, gotRes.Name, wantRes.Name)
eq(t, verror.ErrorID(gotRes.Err), verror.ErrorID(wantRes.Err))
durEq(t, gotRes.Dur, wantRes.Dur)
}
}
var (
errCanceled = verror.NewErrCanceled(nil)
errTimeout = verror.NewErrTimeout(nil)
)
func TestPingInParallel(t *testing.T) {
ctx, shutdown := test.V23Init()
defer shutdown()
var cleanups []func()
defer func() {
for _, cleanup := range cleanups {
cleanup()
}
}()
serve := func(delay time.Duration) string {
name, cleanup := startPingableServer(t, ctx, delay)
cleanups = append(cleanups, cleanup)
return name
}
// Start some pingable servers, each with its own delay.
a, b, c := serve(ms(10)), serve(ms(50)), serve(ms(90))
// NOTE(sadovsky): The first RPC to a given server carries a ~20ms overhead
// (ouch!); subsequent RPCs have ~5ms overhead (ouch, but not as much ouch).
// We do one round of PingInParallel to warm up all the servers so that we can
// use tighter bounds on expected durations in pingResultsEq.
res, err := ping.PingInParallel(ctx, []string{a, b, c}, ms(200), ms(200))
eq(t, err, nil)
start := time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, ms(200), ms(200))
eq(t, err, nil)
// The slowest server has a 90ms delay, so PingInParallel should complete
// after roughly 90ms, i.e. well before the 200ms timeout.
durEq(t, time.Now().Sub(start), ms(90))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(50)},
c: {Name: c, Dur: ms(90)},
})
// No slack.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, 0, ms(200))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(10))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(10), Err: errCanceled},
c: {Name: c, Dur: ms(10), Err: errCanceled},
})
// 60ms slack: enough for b, but not for c.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, ms(60), ms(200))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(10+60))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(50)},
c: {Name: c, Dur: ms(10 + 60), Err: errCanceled},
})
// Same as above, but with names in a different order to demonstrate that the
// order doesn't matter.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{c, b, a}, ms(60), ms(200))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(10+60))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(50)},
c: {Name: c, Dur: ms(10 + 60), Err: errCanceled},
})
// 60ms slack, 30ms timeout: slack is enough for b, but timeout trumps slack.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, ms(60), ms(30))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(30))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(30), Err: errTimeout},
c: {Name: c, Dur: ms(30), Err: errTimeout},
})
// 200ms slack, 0ms timeout: no server responds in time.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, ms(200), ms(0))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(0))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(0), Err: errTimeout},
b: {Name: b, Dur: ms(0), Err: errTimeout},
c: {Name: c, Dur: ms(0), Err: errTimeout},
})
// 200ms slack, 70ms timeout: slack is enough for both b and c, but timeout
// causes c to fail.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{a, b, c}, ms(200), ms(70))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(70))
pingResultsEq(t, res, map[string]ping.PingResult{
a: {Name: a, Dur: ms(10)},
b: {Name: b, Dur: ms(50)},
c: {Name: c, Dur: ms(70), Err: errTimeout},
})
// No servers to ping.
start = time.Now()
res, err = ping.PingInParallel(ctx, []string{}, ms(20), ms(0))
eq(t, err, nil)
durEq(t, time.Now().Sub(start), ms(0))
pingResultsEq(t, res, map[string]ping.PingResult{})
}
////////////////////////////////////////////////////////////////////////////////
// TestSortByDur
var errPingFailed = errors.New("ping failed")
func TestSortByDur(t *testing.T) {
var m map[string]ping.PingResult
// Empty map.
eq(t, ping.SortByDur(m), []ping.PingResult{})
// Map with one failed ping.
m = map[string]ping.PingResult{
"a": {Name: "a", Err: errPingFailed},
}
eq(t, ping.SortByDur(m), []ping.PingResult{})
// Map with two failed pings.
m = map[string]ping.PingResult{
"a": {Name: "a", Err: errPingFailed},
"b": {Name: "b", Err: errPingFailed},
}
eq(t, ping.SortByDur(m), []ping.PingResult{})
// Map with one OK ping.
aRes := ping.PingResult{Name: "a", Dur: 5}
m = map[string]ping.PingResult{
"a": aRes,
}
eq(t, ping.SortByDur(m), []ping.PingResult{aRes})
// Map with two OK pings.
bRes := ping.PingResult{Name: "b", Dur: 4}
m = map[string]ping.PingResult{
"a": aRes,
"b": bRes,
}
eq(t, ping.SortByDur(m), []ping.PingResult{bRes, aRes}) // b.Dur < a.Dur
// Map with mix of OK and failed pings.
m = map[string]ping.PingResult{
"a": aRes,
"b": bRes,
"c": {Name: "c", Err: errPingFailed},
}
eq(t, ping.SortByDur(m), []ping.PingResult{bRes, aRes}) // b.Dur < a.Dur
// Map with lots of OK and failed pings.
m = map[string]ping.PingResult{}
for i := 0; i < 10; i++ {
name := strconv.Itoa(i)
err := error(nil)
if i%2 == 0 {
err = errPingFailed
}
m[name] = ping.PingResult{Name: name, Err: err, Dur: time.Duration(i % 3)}
}
eq(t, ping.SortByDur(m), []ping.PingResult{
{Name: "3", Dur: 0},
{Name: "9", Dur: 0},
{Name: "1", Dur: 1},
{Name: "7", Dur: 1},
{Name: "5", Dur: 2},
})
}