github.com/google/cloudprober@v0.11.3/targets/resolver/resolver_test.go (about)

     1  // Copyright 2017 The Cloudprober Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package resolver
    16  
    17  import (
    18  	"fmt"
    19  	"net"
    20  	"runtime"
    21  	"runtime/debug"
    22  	"sync"
    23  	"testing"
    24  	"time"
    25  )
    26  
    27  type resolveBackendWithTracking struct {
    28  	nameToIP map[string][]net.IP
    29  	called   int
    30  	mu       sync.Mutex
    31  }
    32  
    33  func (b *resolveBackendWithTracking) resolve(name string) ([]net.IP, error) {
    34  	b.mu.Lock()
    35  	defer b.mu.Unlock()
    36  	b.called++
    37  	return b.nameToIP[name], nil
    38  }
    39  
    40  func (b *resolveBackendWithTracking) calls() int {
    41  	b.mu.Lock()
    42  	defer b.mu.Unlock()
    43  	return b.called
    44  }
    45  
    46  func verify(testCase string, t *testing.T, ip, expectedIP net.IP, backendCalls, expectedBackendCalls int, err error) {
    47  	if err != nil {
    48  		t.Errorf("%s: Error while resolving. Err: %v", testCase, err)
    49  	}
    50  	if !ip.Equal(expectedIP) {
    51  		t.Errorf("%s: Got wrong IP address. Got: %s, Expected: %s", testCase, ip, expectedIP)
    52  	}
    53  	if backendCalls != expectedBackendCalls {
    54  		t.Errorf("%s: Backend calls: %d, Expected: %d", testCase, backendCalls, expectedBackendCalls)
    55  	}
    56  }
    57  
    58  // waitForChannelOrFail reads the result from the channel and fails if it
    59  // wasn't received within the timeout.
    60  func waitForChannelOrFail(t *testing.T, c <-chan bool, timeout time.Duration) bool {
    61  	select {
    62  	case b := <-c:
    63  		return b
    64  	case <-time.After(timeout):
    65  		t.Error("Channel didn't close. Stack-trace: ", debug.Stack())
    66  		return false
    67  	}
    68  }
    69  
    70  func TestResolveWithMaxAge(t *testing.T) {
    71  	b := &resolveBackendWithTracking{
    72  		nameToIP: make(map[string][]net.IP),
    73  	}
    74  	r := &Resolver{
    75  		cache:   make(map[string]*cacheRecord),
    76  		resolve: b.resolve,
    77  	}
    78  
    79  	testHost := "hostA"
    80  	expectedIP := net.ParseIP("1.2.3.4")
    81  	b.nameToIP[testHost] = []net.IP{expectedIP}
    82  
    83  	// Resolve a host, there is no cache, a backend call should be made
    84  	expectedBackendCalls := 1
    85  	refreshed := make(chan bool, 2)
    86  	ip, err := r.resolveWithMaxAge(testHost, 4, 60*time.Second, refreshed)
    87  	verify("first-run-no-cache", t, ip, expectedIP, b.calls(), expectedBackendCalls, err)
    88  	// First Resolve calls refresh twice. Once for init (which succeeds), and
    89  	// then again for refreshing, which is not needed. Hence the results are true
    90  	// and then false.
    91  	if !waitForChannelOrFail(t, refreshed, time.Second) {
    92  		t.Errorf("refreshed returned false, want true")
    93  	}
    94  	if waitForChannelOrFail(t, refreshed, time.Second) {
    95  		t.Errorf("refreshed returned true, want false")
    96  	}
    97  
    98  	// Resolve same host again, it should come from cache, no backend call
    99  	newExpectedIP := net.ParseIP("1.2.3.6")
   100  	b.nameToIP[testHost] = []net.IP{newExpectedIP}
   101  	ip, err = r.resolveWithMaxAge(testHost, 4, 60*time.Second, refreshed)
   102  	verify("second-run-from-cache", t, ip, expectedIP, b.calls(), expectedBackendCalls, err)
   103  	if waitForChannelOrFail(t, refreshed, time.Second) {
   104  		t.Errorf("refreshed returned true, want false")
   105  	}
   106  
   107  	// Resolve same host again with maxAge=0, it will issue an asynchronous (hence no increment
   108  	// in expectedBackenddCalls) backend call
   109  	ip, err = r.resolveWithMaxAge(testHost, 4, 0*time.Second, refreshed)
   110  	verify("third-run-expire-cache", t, ip, expectedIP, b.calls(), expectedBackendCalls, err)
   111  	if !waitForChannelOrFail(t, refreshed, time.Second) {
   112  		t.Errorf("refreshed returned false, want true")
   113  	}
   114  	// Now that refresh has happened, we should see a new IP.
   115  	expectedIP = newExpectedIP
   116  	expectedBackendCalls++
   117  	ip, err = r.resolveWithMaxAge(testHost, 4, 60*time.Second, refreshed)
   118  	verify("fourth-run-new-result", t, ip, expectedIP, b.calls(), expectedBackendCalls, err)
   119  	if waitForChannelOrFail(t, refreshed, time.Second) {
   120  		t.Errorf("refreshed returned true, want false")
   121  	}
   122  }
   123  
   124  func TestResolveErr(t *testing.T) {
   125  	cnt := 0
   126  	r := &Resolver{
   127  		cache: make(map[string]*cacheRecord),
   128  		resolve: func(name string) ([]net.IP, error) {
   129  			cnt++
   130  			if cnt == 2 {
   131  				return nil, fmt.Errorf("time to return error, cnt: %d", cnt)
   132  			}
   133  			return []net.IP{net.ParseIP("0.0.0.0")}, nil
   134  		},
   135  	}
   136  	refreshed := make(chan bool, 2)
   137  	// cnt=0; returning 0.0.0.0.
   138  	_, err := r.resolveWithMaxAge("testHost", 4, 60*time.Second, refreshed)
   139  	if err != nil {
   140  		t.Logf("Err: %v\n", err)
   141  		t.Errorf("Expected no error, got error")
   142  	}
   143  	// First Resolve calls refresh twice. Once for init (which succeeds), and
   144  	// then again for refreshing, which is not needed. Hence the results are true
   145  	// and then false.
   146  	if !waitForChannelOrFail(t, refreshed, time.Second) {
   147  		t.Errorf("refreshed returned false, want true")
   148  	}
   149  	if waitForChannelOrFail(t, refreshed, time.Second) {
   150  		t.Errorf("refreshed returned true, want false")
   151  	}
   152  	// cnt=1, returning 0.0.0.0, but updating the cache record asynchronously to contain the
   153  	// error returned for cnt=2.
   154  	_, err = r.resolveWithMaxAge("testHost", 4, 0*time.Second, refreshed)
   155  	if err != nil {
   156  		t.Logf("Err: %v\n", err)
   157  		t.Errorf("Expected no error, got error")
   158  	}
   159  	if !waitForChannelOrFail(t, refreshed, time.Second) {
   160  		t.Errorf("refreshed returned false, want true")
   161  	}
   162  	// cache record contains an error, and we should therefore expect an error.
   163  	// This call for resolve will have cnt=2, and the asynchronous call to update the cache will
   164  	// therefore update it to contain 0.0.0.0, which should be returned by the next call.
   165  	_, err = r.resolveWithMaxAge("testHost", 4, 0*time.Second, refreshed)
   166  	if err == nil {
   167  		t.Errorf("Expected error, got no error")
   168  	}
   169  	if !waitForChannelOrFail(t, refreshed, time.Second) {
   170  		t.Errorf("refreshed returned false, want true")
   171  	}
   172  	// cache record now contains 0.0.0.0 again.
   173  	_, err = r.resolveWithMaxAge("testHost", 4, 0*time.Second, refreshed)
   174  	if err != nil {
   175  		t.Logf("Err: %v\n", err)
   176  		t.Errorf("Expected no error, got error")
   177  	}
   178  	if !waitForChannelOrFail(t, refreshed, time.Second) {
   179  		t.Errorf("refreshed returned false, want true")
   180  	}
   181  }
   182  
   183  func TestResolveIPv6(t *testing.T) {
   184  	b := &resolveBackendWithTracking{
   185  		nameToIP: make(map[string][]net.IP),
   186  	}
   187  	r := &Resolver{
   188  		cache:   make(map[string]*cacheRecord),
   189  		resolve: b.resolve,
   190  	}
   191  
   192  	testHost := "hostA"
   193  	expectedIPv4 := net.ParseIP("1.2.3.4")
   194  	expectedIPv6 := net.ParseIP("::1")
   195  	b.nameToIP[testHost] = []net.IP{expectedIPv4, expectedIPv6}
   196  
   197  	ip, err := r.Resolve(testHost, 4)
   198  	expectedBackendCalls := 1
   199  	verify("ipv4-address-not-as-expected", t, ip, expectedIPv4, b.calls(), expectedBackendCalls, err)
   200  
   201  	// This will come from cache this time, so no new backend calls.
   202  	ip, err = r.Resolve(testHost, 6)
   203  	verify("ipv6-address-not-as-expected", t, ip, expectedIPv6, b.calls(), expectedBackendCalls, err)
   204  
   205  	// No IP version specified, should return IPv4 as IPv4 gets preference.
   206  	// This will come from cache this time, so no new backend calls.
   207  	ip, err = r.Resolve(testHost, 0)
   208  	verify("ipv0-address-not-as-expected", t, ip, expectedIPv4, b.calls(), expectedBackendCalls, err)
   209  
   210  	// New host, with no IPv4 address
   211  	testHost = "hostB"
   212  	expectedIPv6 = net.ParseIP("::2")
   213  	b.nameToIP[testHost] = []net.IP{expectedIPv6}
   214  
   215  	ip, err = r.Resolve(testHost, 4)
   216  	expectedBackendCalls++
   217  	if err == nil {
   218  		t.Errorf("resolved IPv4 address for an IPv6 only host")
   219  	}
   220  
   221  	// This will come from cache this time, so no new backend calls.
   222  	ip, err = r.Resolve(testHost, 6)
   223  	verify("ipv6-address-not-as-expected", t, ip, expectedIPv6, b.calls(), expectedBackendCalls, err)
   224  
   225  	// No IP version specified, should return IPv6 as there is no IPv4 address for this host.
   226  	// This will come from cache this time, so no new backend calls.
   227  	ip, err = r.Resolve(testHost, 0)
   228  	verify("ipv0-address-not-as-expected", t, ip, expectedIPv6, b.calls(), expectedBackendCalls, err)
   229  }
   230  
   231  // TestConcurrentInit tests that multiple Resolves in parallel on the same
   232  // target all return the same answer, and cause just 1 call to resolve.
   233  func TestConcurrentInit(t *testing.T) {
   234  	cnt := 0
   235  	resolveWait := make(chan bool)
   236  	r := &Resolver{
   237  		cache: make(map[string]*cacheRecord),
   238  		resolve: func(name string) ([]net.IP, error) {
   239  			cnt++
   240  			// The first call should be blocked on resolveWait.
   241  			if cnt == 1 {
   242  				<-resolveWait
   243  				return []net.IP{net.ParseIP("0.0.0.0")}, nil
   244  			}
   245  			// The 2nd call should never happen.
   246  			return nil, fmt.Errorf("resolve should be called just once, cnt: %d", cnt)
   247  		},
   248  	}
   249  	// 5 because first resolve calls refresh twice.
   250  	refreshed := make(chan bool, 5)
   251  	var wg sync.WaitGroup
   252  	for i := 0; i < 4; i++ {
   253  		wg.Add(1)
   254  		go func() {
   255  			_, err := r.resolveWithMaxAge("testHost", 4, 60*time.Second, refreshed)
   256  			if err != nil {
   257  				t.Logf("Err: %v\n", err)
   258  				t.Errorf("Expected no error, got error")
   259  			}
   260  			wg.Done()
   261  		}()
   262  	}
   263  	// Give offline update goroutines a chance.
   264  	// If we call resolve more than once, this will make those resolves fail.
   265  	runtime.Gosched()
   266  	time.Sleep(1 * time.Millisecond)
   267  	// Makes one of the resolve goroutines unblock refresh.
   268  	resolveWait <- true
   269  	resolvedCount := 0
   270  	// 5 because first resolve calls refresh twice.
   271  	for i := 0; i < 5; i++ {
   272  		if waitForChannelOrFail(t, refreshed, time.Second) {
   273  			resolvedCount++
   274  		}
   275  	}
   276  	if resolvedCount != 1 {
   277  		t.Errorf("resolvedCount=%v, want 1", resolvedCount)
   278  	}
   279  	wg.Wait()
   280  }
   281  
   282  // Set up benchmarks. Apart from performance stats it verifies the library's behavior during concurrent
   283  // runs. It's kind of important as we use mutexes a lot, even though never in long running path, e.g.
   284  // actual backend resolver is called outside mutexes.
   285  //
   286  // Use following command to run benchmark tests:
   287  // BMC=6 BMT=4 // 6 CPUs, 4 sec
   288  // blaze test --config=gotsan :resolver_test --test_arg=-test.bench=. \
   289  //   --test_arg=-test.benchtime=${BMT}s --test_arg=-test.cpu=$BMC
   290  type resolveBackendBenchmark struct {
   291  	delay   time.Duration // artificial delay in resolving
   292  	callCnt int64
   293  	t       time.Time
   294  }
   295  
   296  func (rb *resolveBackendBenchmark) resolve(name string) ([]net.IP, error) {
   297  	rb.callCnt++
   298  	fmt.Printf("Time since initiation: %s\n", time.Since(rb.t))
   299  	if rb.delay != 0 {
   300  		time.Sleep(rb.delay)
   301  	}
   302  	return []net.IP{net.ParseIP("0.0.0.0")}, nil
   303  }
   304  
   305  func BenchmarkResolve(b *testing.B) {
   306  	rb := &resolveBackendBenchmark{
   307  		delay: 10 * time.Millisecond,
   308  		t:     time.Now(),
   309  	}
   310  	r := &Resolver{
   311  		cache:   make(map[string]*cacheRecord),
   312  		resolve: rb.resolve,
   313  	}
   314  	// RunParallel executes its body in parallel, in multiple goroutines. Parallelism is controlled by
   315  	// the test -cpu (test.cpu) flag (default is GOMAXPROCS). So if benchmarks runs N times, that N
   316  	// is spread over these goroutines.
   317  	//
   318  	// Example benchmark results with cpu=6
   319  	// BenchmarkResolve-6  3000	   1689466 ns/op
   320  	//
   321  	// 3000 is the total number of iterations (N) and it took on an average 1.69ms per iteration.
   322  	// Total run time = 1.69 x 3000 = 5.07s. Since each goroutine executed 3000/6 or 500 iterations, with
   323  	// each iteration taking 10ms because of artificial delay, each goroutine will take at least 5s, very
   324  	// close to what benchmark found out.
   325  	b.RunParallel(func(pb *testing.PB) {
   326  		// Next() returns true if there are more iterations to execute.
   327  		for pb.Next() {
   328  			r.resolveWithMaxAge("test", 4, 500*time.Millisecond, nil)
   329  			time.Sleep(10 * time.Millisecond)
   330  		}
   331  	})
   332  	fmt.Printf("Called backend resolve %d times\n", rb.callCnt)
   333  }