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 }