github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/cache/memcached_refresh_test.go (about) 1 package cache 2 3 import ( 4 "context" 5 "net" 6 "slices" 7 "sync" 8 "testing" 9 "testing/synctest" 10 "time" 11 12 "github.com/stretchr/testify/require" 13 ) 14 15 // TestRefreshServers_PeriodicRefresh tests that DNS resolution happens periodically. 16 func TestRefreshServers_PeriodicRefresh(t *testing.T) { 17 synctest.Test(t, func(t *testing.T) { 18 resolver := newMockResolver() 19 resolver.setResponse("memcached.example.com", "10.0.1.1", "10.0.1.2") 20 21 ctx, cancel := context.WithCancel(context.Background()) 22 defer cancel() 23 24 client, err := NewMemcachedClient(ctx, MemcachedConfig{ 25 Servers: []string{"memcached.example.com:11211"}, 26 Logger: testLogger(), 27 RefreshInterval: 10 * time.Second, 28 Resolver: resolver, 29 }) 30 require.NoError(t, err) 31 require.NotNil(t, client) 32 33 // Wait for the client to be initialized 34 synctest.Wait() 35 36 // Clear initial lookup 37 resolver.clearLookups() 38 39 // Change DNS response 40 resolver.setResponse("memcached.example.com", "10.0.2.1", "10.0.2.2", "10.0.2.3") 41 42 // Advance time by 10 seconds to trigger first refresh 43 time.Sleep(10 * time.Second) 44 synctest.Wait() 45 46 // Verify DNS was queried 47 lookups := resolver.getLookups() 48 require.Contains(t, lookups, "memcached.example.com") 49 50 // Verify server list was updated with new IPs 51 var addrs []string 52 53 err = client.serverList.Each(func(addr net.Addr) error { 54 addrs = append(addrs, addr.String()) 55 return nil 56 }) 57 require.NoError(t, err) 58 require.ElementsMatch(t, []string{"10.0.2.1:11211", "10.0.2.2:11211", "10.0.2.3:11211"}, addrs) 59 60 // Clear lookups and advance time again 61 resolver.clearLookups() 62 resolver.setResponse("memcached.example.com", "10.0.3.1") 63 64 // Allow another refresh to happen. 65 time.Sleep(10 * time.Second) 66 synctest.Wait() 67 68 // Verify second refresh happened 69 lookups = resolver.getLookups() 70 require.Contains(t, lookups, "memcached.example.com") 71 72 // Verify server list updated again 73 addrs = nil 74 err = client.serverList.Each(func(addr net.Addr) error { 75 addrs = append(addrs, addr.String()) 76 return nil 77 }) 78 require.NoError(t, err) 79 require.Equal(t, []string{"10.0.3.1:11211"}, addrs) 80 81 // Cancel context to stop refresh 82 cancel() 83 synctest.Wait() 84 }) 85 } 86 87 // TestRefreshServers_MultipleHosts tests refreshing multiple hostnames. 88 func TestRefreshServers_MultipleHosts(t *testing.T) { 89 synctest.Test(t, func(t *testing.T) { 90 resolver := newMockResolver() 91 resolver.setResponse("cache1.example.com", "10.0.1.1") 92 resolver.setResponse("cache2.example.com", "10.0.2.1", "10.0.2.2") 93 94 ctx, cancel := context.WithCancel(context.Background()) 95 defer cancel() 96 97 client, err := NewMemcachedClient(ctx, MemcachedConfig{ 98 Servers: []string{ 99 "cache1.example.com:11211", 100 "cache2.example.com:11211", 101 "192.168.1.1:11211", // IP address should be used directly 102 }, 103 Logger: testLogger(), 104 RefreshInterval: 5 * time.Second, 105 Resolver: resolver, 106 }) 107 require.NoError(t, err) 108 require.NotNil(t, client) 109 110 synctest.Wait() 111 112 // Verify initial server list 113 var addrs []string 114 115 err = client.serverList.Each(func(addr net.Addr) error { 116 addrs = append(addrs, addr.String()) 117 return nil 118 }) 119 require.NoError(t, err) 120 require.ElementsMatch(t, []string{ 121 "10.0.1.1:11211", 122 "10.0.2.1:11211", 123 "10.0.2.2:11211", 124 "192.168.1.1:11211", 125 }, addrs) 126 127 // Update DNS responses 128 resolver.clearLookups() 129 resolver.setResponse("cache1.example.com", "10.0.1.1", "10.0.1.2") 130 resolver.setResponse("cache2.example.com", "10.0.2.3") 131 132 // Trigger refresh 133 time.Sleep(5 * time.Second) 134 synctest.Wait() 135 136 // Verify lookups happened for hostnames but not IP 137 lookups := resolver.getLookups() 138 require.Contains(t, lookups, "cache1.example.com") 139 require.Contains(t, lookups, "cache2.example.com") 140 require.NotContains(t, lookups, "192.168.1.1") 141 142 // Verify updated server list 143 addrs = nil 144 err = client.serverList.Each(func(addr net.Addr) error { 145 addrs = append(addrs, addr.String()) 146 return nil 147 }) 148 require.NoError(t, err) 149 require.ElementsMatch(t, []string{ 150 "10.0.1.1:11211", 151 "10.0.1.2:11211", 152 "10.0.2.3:11211", 153 "192.168.1.1:11211", 154 }, addrs) 155 156 cancel() 157 synctest.Wait() 158 }) 159 } 160 161 // TestRefreshServers_DNSFailure tests handling of DNS resolution failures. 162 func TestRefreshServers_DNSFailure(t *testing.T) { 163 synctest.Test(t, func(t *testing.T) { 164 resolver := newMockResolver() 165 resolver.setResponse("cache.example.com", "10.0.1.1", "10.0.1.2") 166 167 ctx, cancel := context.WithCancel(context.Background()) 168 defer cancel() 169 170 client, err := NewMemcachedClient(ctx, MemcachedConfig{ 171 Servers: []string{"cache.example.com:11211"}, 172 Logger: testLogger(), 173 RefreshInterval: 5 * time.Second, 174 Resolver: resolver, 175 }) 176 require.NoError(t, err) 177 synctest.Wait() 178 179 // Verify initial servers 180 var addrs []string 181 182 err = client.serverList.Each(func(addr net.Addr) error { 183 addrs = append(addrs, addr.String()) 184 return nil 185 }) 186 require.NoError(t, err) 187 require.ElementsMatch(t, []string{"10.0.1.1:11211", "10.0.1.2:11211"}, addrs) 188 189 // Make DNS fail 190 resolver.setError("cache.example.com", &net.DNSError{ 191 Err: "temporary failure", 192 Name: "cache.example.com", 193 IsTemporary: true, 194 }) 195 196 // Trigger refresh 197 time.Sleep(5 * time.Second) 198 synctest.Wait() 199 200 // Verify servers remain unchanged (old servers still work) 201 addrs = nil 202 err = client.serverList.Each(func(addr net.Addr) error { 203 addrs = append(addrs, addr.String()) 204 return nil 205 }) 206 require.NoError(t, err) 207 require.ElementsMatch(t, []string{"10.0.1.1:11211", "10.0.1.2:11211"}, addrs) 208 209 // Restore DNS and add new IP 210 resolver.setResponse("cache.example.com", "10.0.2.1") 211 212 // Trigger another refresh 213 time.Sleep(5 * time.Second) 214 synctest.Wait() 215 216 // Verify servers updated to new IP 217 addrs = nil 218 err = client.serverList.Each(func(addr net.Addr) error { 219 addrs = append(addrs, addr.String()) 220 return nil 221 }) 222 require.NoError(t, err) 223 require.Equal(t, []string{"10.0.2.1:11211"}, addrs) 224 225 cancel() 226 synctest.Wait() 227 }) 228 } 229 230 // TestRefreshServers_ContextCancellation tests that refresh stops when context is cancelled. 231 func TestRefreshServers_ContextCancellation(t *testing.T) { 232 synctest.Test(t, func(t *testing.T) { 233 resolver := newMockResolver() 234 resolver.setResponse("cache.example.com", "10.0.1.1") 235 236 ctx, cancel := context.WithCancel(context.Background()) 237 238 _, err := NewMemcachedClient(ctx, MemcachedConfig{ 239 Servers: []string{"cache.example.com:11211"}, 240 Logger: testLogger(), 241 RefreshInterval: 5 * time.Second, 242 Resolver: resolver, 243 }) 244 require.NoError(t, err) 245 synctest.Wait() 246 247 // Clear lookups 248 resolver.clearLookups() 249 250 // Advance time less than refresh interval, then cancel 251 time.Sleep(3 * time.Second) 252 cancel() 253 254 // Give the refresh goroutine time to process cancellation 255 synctest.Wait() 256 257 // Verify no refresh happened (context cancelled before 5 second interval) 258 lookups := resolver.getLookups() 259 require.Empty(t, lookups, "No DNS lookups should happen after context cancellation") 260 }) 261 } 262 263 // TestRefreshServers_CustomInterval tests that custom refresh intervals work. 264 func TestRefreshServers_CustomInterval(t *testing.T) { 265 synctest.Test(t, func(t *testing.T) { 266 resolver := newMockResolver() 267 resolver.setResponse("cache.example.com", "10.0.1.1") 268 269 ctx, cancel := context.WithCancel(context.Background()) 270 defer cancel() 271 272 _, err := NewMemcachedClient(ctx, MemcachedConfig{ 273 Servers: []string{"cache.example.com:11211"}, 274 Logger: testLogger(), 275 RefreshInterval: 30 * time.Second, // Custom interval 276 Resolver: resolver, 277 }) 278 require.NoError(t, err) 279 synctest.Wait() 280 281 resolver.clearLookups() 282 283 // Advance time less than interval - should not refresh 284 time.Sleep(20 * time.Second) 285 synctest.Wait() 286 287 lookups := resolver.getLookups() 288 require.Empty(t, lookups, "Should not refresh before interval") 289 290 // Advance past interval 291 time.Sleep(15 * time.Second) // Total: 35 seconds 292 synctest.Wait() 293 294 lookups = resolver.getLookups() 295 require.Contains(t, lookups, "cache.example.com", "Should refresh after interval") 296 297 cancel() 298 synctest.Wait() 299 }) 300 } 301 302 // TestResolveServers_DirectIPAddresses tests that IP addresses are used without resolution. 303 func TestResolveServers_DirectIPAddresses(t *testing.T) { 304 resolver := newMockResolver() 305 logger := testLogger() 306 307 servers := []string{ 308 "192.168.1.1:11211", 309 "10.0.0.1:11211", 310 "[2001:db8::1]:11211", 311 } 312 313 resolved := resolveServers(context.Background(), servers, resolver, logger) 314 315 // Verify IP addresses are returned as-is 316 require.ElementsMatch(t, servers, resolved) 317 318 // Verify no DNS lookups happened 319 lookups := resolver.getLookups() 320 require.Empty(t, lookups, "IP addresses should not trigger DNS lookups") 321 } 322 323 // TestResolveServers_MixedHostnamesAndIPs tests resolving a mix of hostnames and IPs. 324 func TestResolveServers_MixedHostnamesAndIPs(t *testing.T) { 325 resolver := newMockResolver() 326 resolver.setResponse("cache.example.com", "10.1.1.1", "10.1.1.2") 327 328 logger := testLogger() 329 330 servers := []string{ 331 "cache.example.com:11211", 332 "192.168.1.1:11211", 333 "[::1]:11211", 334 } 335 336 resolved := resolveServers(context.Background(), servers, resolver, logger) 337 338 require.ElementsMatch(t, []string{ 339 "10.1.1.1:11211", 340 "10.1.1.2:11211", 341 "192.168.1.1:11211", 342 "[::1]:11211", 343 }, resolved) 344 345 // Verify only hostname was looked up 346 lookups := resolver.getLookups() 347 require.Equal(t, []string{"cache.example.com"}, lookups) 348 } 349 350 // mockResolver is a controllable DNS resolver for testing. 351 type mockResolver struct { 352 mu sync.RWMutex 353 responses map[string][]net.IP 354 errors map[string]error 355 lookups []string // Track lookup history 356 } 357 358 func newMockResolver() *mockResolver { 359 return &mockResolver{ 360 responses: make(map[string][]net.IP), 361 errors: make(map[string]error), 362 lookups: make([]string, 0), 363 } 364 } 365 366 func (m *mockResolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, error) { 367 m.mu.Lock() 368 defer m.mu.Unlock() 369 370 m.lookups = append(m.lookups, host) 371 372 if err, ok := m.errors[host]; ok { 373 return nil, err 374 } 375 376 if ips, ok := m.responses[host]; ok { 377 return ips, nil 378 } 379 380 return nil, &net.DNSError{ 381 Err: "no such host", 382 Name: host, 383 IsNotFound: true, 384 } 385 } 386 387 // setResponse sets the IP addresses that will be returned for a given hostname. 388 // This also clears any error that was previously set for this host. 389 func (m *mockResolver) setResponse(host string, ips ...string) { 390 m.mu.Lock() 391 defer m.mu.Unlock() 392 393 netIPs := make([]net.IP, len(ips)) 394 for i, ip := range ips { 395 netIPs[i] = net.ParseIP(ip) 396 } 397 398 m.responses[host] = netIPs 399 delete(m.errors, host) // Clear any error for this host 400 } 401 402 // setError sets an error that will be returned for a given hostname. 403 func (m *mockResolver) setError(host string, err error) { 404 m.mu.Lock() 405 defer m.mu.Unlock() 406 407 m.errors[host] = err 408 } 409 410 // clearLookups clears the lookup history. 411 func (m *mockResolver) clearLookups() { 412 m.mu.Lock() 413 defer m.mu.Unlock() 414 415 m.lookups = nil 416 } 417 418 // getLookups returns the list of hostnames that were looked up. 419 func (m *mockResolver) getLookups() []string { 420 m.mu.RLock() 421 defer m.mu.RUnlock() 422 423 return slices.Clone(m.lookups) 424 }