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  }