github.com/grafana/pyroscope@v1.18.0/pkg/util/servicediscovery/ring_test.go (about)

     1  // SPDX-License-Identifier: AGPL-3.0-only
     2  
     3  package servicediscovery
     4  
     5  import (
     6  	"context"
     7  	"sort"
     8  	"sync"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/grafana/dskit/kv/consul"
    14  	"github.com/grafana/dskit/ring"
    15  	"github.com/grafana/dskit/services"
    16  	"github.com/grafana/dskit/test"
    17  	"github.com/stretchr/testify/assert"
    18  	"github.com/stretchr/testify/require"
    19  )
    20  
    21  func TestRingServiceDiscovery_WithoutMaxUsedInstances(t *testing.T) {
    22  	const (
    23  		ringKey         = "test"
    24  		ringCheckPeriod = 100 * time.Millisecond // Check very frequently to speed up the test.
    25  	)
    26  
    27  	// Use an in-memory KV store.
    28  	inmem, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
    29  	t.Cleanup(func() { _ = closer.Close() })
    30  
    31  	// Create a ring client.
    32  	ringCfg := ring.Config{HeartbeatTimeout: time.Minute, ReplicationFactor: 1}
    33  	ringClient, err := ring.NewWithStoreClientAndStrategy(ringCfg, "test", ringKey, inmem, ring.NewDefaultReplicationStrategy(), nil, log.NewNopLogger())
    34  	require.NoError(t, err)
    35  
    36  	// Create an empty ring.
    37  	ctx := context.Background()
    38  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
    39  		return ring.NewDesc(), true, nil
    40  	}))
    41  
    42  	// Mock a receiver to keep track of all notified addresses.
    43  	receiver := newNotificationsReceiverMock()
    44  
    45  	sd := NewRing(ringClient, ringCheckPeriod, 0, receiver)
    46  
    47  	// Start the service discovery.
    48  	require.NoError(t, services.StartAndAwaitRunning(ctx, sd))
    49  	t.Cleanup(func() {
    50  		require.NoError(t, services.StopAndAwaitTerminated(ctx, sd))
    51  	})
    52  
    53  	// Wait some time, we expect no address notified because the ring is empty.
    54  	time.Sleep(time.Second)
    55  	require.Empty(t, receiver.getDiscoveredInstances())
    56  
    57  	// Register some instances.
    58  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
    59  		desc := in.(*ring.Desc)
    60  		desc.AddIngester("instance-1", "127.0.0.1", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
    61  		desc.AddIngester("instance-2", "127.0.0.2", "", nil, ring.PENDING, time.Now(), false, time.Now())
    62  		desc.AddIngester("instance-3", "127.0.0.3", "", nil, ring.JOINING, time.Now(), false, time.Now())
    63  		desc.AddIngester("instance-4", "127.0.0.4", "", nil, ring.LEAVING, time.Now(), false, time.Now())
    64  		return desc, true, nil
    65  	}))
    66  
    67  	test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}}, func() interface{} {
    68  		return receiver.getDiscoveredInstances()
    69  	})
    70  
    71  	// Register more instances.
    72  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
    73  		desc := in.(*ring.Desc)
    74  		desc.AddIngester("instance-5", "127.0.0.5", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
    75  		desc.AddIngester("instance-6", "127.0.0.6", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
    76  		return desc, true, nil
    77  	}))
    78  
    79  	test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}, {"127.0.0.5", true}, {"127.0.0.6", true}}, func() interface{} {
    80  		return receiver.getDiscoveredInstances()
    81  	})
    82  
    83  	// Unregister some instances.
    84  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
    85  		desc := in.(*ring.Desc)
    86  		desc.RemoveIngester("instance-1")
    87  		desc.RemoveIngester("instance-6")
    88  		return desc, true, nil
    89  	}))
    90  
    91  	test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} {
    92  		return receiver.getDiscoveredInstances()
    93  	})
    94  
    95  	// A non-active instance switches to active.
    96  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
    97  		desc := in.(*ring.Desc)
    98  		instance := desc.Ingesters["instance-2"]
    99  		instance.State = ring.ACTIVE
   100  		desc.Ingesters["instance-2"] = instance
   101  		return desc, true, nil
   102  	}))
   103  
   104  	test.Poll(t, time.Second, []Instance{{"127.0.0.2", true}, {"127.0.0.5", true}}, func() interface{} {
   105  		return receiver.getDiscoveredInstances()
   106  	})
   107  
   108  	// An active becomes unhealthy.
   109  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   110  		desc := in.(*ring.Desc)
   111  		instance := desc.Ingesters["instance-2"]
   112  		instance.Timestamp = time.Now().Add(-2 * ringCfg.HeartbeatTimeout).Unix()
   113  		desc.Ingesters["instance-2"] = instance
   114  		return desc, true, nil
   115  	}))
   116  
   117  	test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} {
   118  		return receiver.getDiscoveredInstances()
   119  	})
   120  }
   121  
   122  func TestRingServiceDiscovery_WithMaxUsedInstances(t *testing.T) {
   123  	const (
   124  		ringKey          = "test"
   125  		ringCheckPeriod  = 100 * time.Millisecond // Check very frequently to speed up the test.
   126  		maxUsedInstances = 2
   127  	)
   128  
   129  	// Use an in-memory KV store.
   130  	inmem, closer := consul.NewInMemoryClient(ring.GetCodec(), log.NewNopLogger(), nil)
   131  	t.Cleanup(func() { _ = closer.Close() })
   132  
   133  	// Create a ring client.
   134  	ringCfg := ring.Config{HeartbeatTimeout: time.Minute, ReplicationFactor: 1}
   135  	ringClient, err := ring.NewWithStoreClientAndStrategy(ringCfg, "test", ringKey, inmem, ring.NewDefaultReplicationStrategy(), nil, log.NewNopLogger())
   136  	require.NoError(t, err)
   137  
   138  	// Create an empty ring.
   139  	ctx := context.Background()
   140  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   141  		return ring.NewDesc(), true, nil
   142  	}))
   143  
   144  	// Mock a receiver to keep track of all notified addresses.
   145  	receiver := newNotificationsReceiverMock()
   146  
   147  	sd := NewRing(ringClient, ringCheckPeriod, maxUsedInstances, receiver)
   148  
   149  	// Start the service discovery.
   150  	require.NoError(t, services.StartAndAwaitRunning(ctx, sd))
   151  	t.Cleanup(func() {
   152  		require.NoError(t, services.StopAndAwaitTerminated(ctx, sd))
   153  	})
   154  
   155  	// Wait some time, we expect no address notified because the ring is empty.
   156  	time.Sleep(time.Second)
   157  	require.Empty(t, receiver.getDiscoveredInstances())
   158  
   159  	// Register some instances.
   160  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   161  		desc := in.(*ring.Desc)
   162  		desc.AddIngester("instance-1", "127.0.0.1", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
   163  		desc.AddIngester("instance-2", "127.0.0.2", "", nil, ring.PENDING, time.Now(), false, time.Now())
   164  		desc.AddIngester("instance-3", "127.0.0.3", "", nil, ring.JOINING, time.Now(), false, time.Now())
   165  		desc.AddIngester("instance-4", "127.0.0.4", "", nil, ring.LEAVING, time.Now(), false, time.Now())
   166  		return desc, true, nil
   167  	}))
   168  
   169  	test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}}, func() interface{} {
   170  		return receiver.getDiscoveredInstances()
   171  	})
   172  
   173  	// Register more instances.
   174  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   175  		desc := in.(*ring.Desc)
   176  		desc.AddIngester("instance-5", "127.0.0.5", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
   177  		desc.AddIngester("instance-6", "127.0.0.6", "", nil, ring.ACTIVE, time.Now(), false, time.Now())
   178  		return desc, true, nil
   179  	}))
   180  
   181  	test.Poll(t, time.Second, []Instance{{"127.0.0.1", true}, {"127.0.0.5", true}, {"127.0.0.6", false}}, func() interface{} {
   182  		return receiver.getDiscoveredInstances()
   183  	})
   184  
   185  	// Unregister some instances.
   186  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   187  		desc := in.(*ring.Desc)
   188  		desc.RemoveIngester("instance-1")
   189  		desc.RemoveIngester("instance-6")
   190  		return desc, true, nil
   191  	}))
   192  
   193  	test.Poll(t, time.Second, []Instance{{"127.0.0.5", true}}, func() interface{} {
   194  		return receiver.getDiscoveredInstances()
   195  	})
   196  
   197  	// Some non-active instances switch to active.
   198  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   199  		desc := in.(*ring.Desc)
   200  		instance := desc.Ingesters["instance-2"]
   201  		instance.State = ring.ACTIVE
   202  		desc.Ingesters["instance-2"] = instance
   203  
   204  		instance = desc.Ingesters["instance-3"]
   205  		instance.State = ring.ACTIVE
   206  		desc.Ingesters["instance-3"] = instance
   207  
   208  		return desc, true, nil
   209  	}))
   210  
   211  	test.Poll(t, time.Second, []Instance{{"127.0.0.2", true}, {"127.0.0.3", true}, {"127.0.0.5", false}}, func() interface{} {
   212  		return receiver.getDiscoveredInstances()
   213  	})
   214  
   215  	// An active becomes unhealthy.
   216  	require.NoError(t, inmem.CAS(ctx, ringKey, func(in interface{}) (out interface{}, retry bool, err error) {
   217  		desc := in.(*ring.Desc)
   218  		instance := desc.Ingesters["instance-2"]
   219  		instance.Timestamp = time.Now().Add(-2 * ringCfg.HeartbeatTimeout).Unix()
   220  		desc.Ingesters["instance-2"] = instance
   221  		return desc, true, nil
   222  	}))
   223  
   224  	test.Poll(t, time.Second, []Instance{{"127.0.0.3", true}, {"127.0.0.5", true}}, func() interface{} {
   225  		return receiver.getDiscoveredInstances()
   226  	})
   227  }
   228  
   229  func TestSelectInUseInstances(t *testing.T) {
   230  	tests := map[string]struct {
   231  		input        []ring.InstanceDesc
   232  		maxInstances int
   233  		expected     []ring.InstanceDesc
   234  	}{
   235  		"should return the input on empty list of instances": {
   236  			input:        nil,
   237  			maxInstances: 3,
   238  			expected:     nil,
   239  		},
   240  		"should return the input on a number of instances < max instances": {
   241  			input:        []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}},
   242  			maxInstances: 3,
   243  			expected:     []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}},
   244  		},
   245  		"should return the input on a number of instances = max instances": {
   246  			input:        []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}},
   247  			maxInstances: 3,
   248  			expected:     []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}},
   249  		},
   250  		"should return a subset of the input on a number of instances > max instances": {
   251  			input:        []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}, {Addr: "4.4.4.4"}, {Addr: "5.5.5.5"}},
   252  			maxInstances: 3,
   253  			expected:     []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "2.2.2.2"}, {Addr: "3.3.3.3"}},
   254  		},
   255  		"should return the input if max instances is 0": {
   256  			input:        []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}},
   257  			maxInstances: 0,
   258  			expected:     []ring.InstanceDesc{{Addr: "1.1.1.1"}, {Addr: "3.3.3.3"}, {Addr: "2.2.2.2"}},
   259  		},
   260  	}
   261  
   262  	for testName, testData := range tests {
   263  		t.Run(testName, func(t *testing.T) {
   264  			assert.Equal(t, testData.expected, selectInUseInstances(testData.input, testData.maxInstances))
   265  		})
   266  	}
   267  }
   268  
   269  type notificationsReceiverMock struct {
   270  	discoveredInstancesMx sync.Mutex
   271  	discoveredInstances   map[string]Instance
   272  }
   273  
   274  func newNotificationsReceiverMock() *notificationsReceiverMock {
   275  	return &notificationsReceiverMock{
   276  		discoveredInstances: map[string]Instance{},
   277  	}
   278  }
   279  
   280  func (r *notificationsReceiverMock) InstanceAdded(instance Instance) {
   281  	r.discoveredInstancesMx.Lock()
   282  	defer r.discoveredInstancesMx.Unlock()
   283  
   284  	r.discoveredInstances[instance.Address] = instance
   285  }
   286  
   287  func (r *notificationsReceiverMock) InstanceRemoved(instance Instance) {
   288  	r.discoveredInstancesMx.Lock()
   289  	defer r.discoveredInstancesMx.Unlock()
   290  
   291  	delete(r.discoveredInstances, instance.Address)
   292  }
   293  
   294  func (r *notificationsReceiverMock) InstanceChanged(instance Instance) {
   295  	r.discoveredInstancesMx.Lock()
   296  	defer r.discoveredInstancesMx.Unlock()
   297  
   298  	r.discoveredInstances[instance.Address] = instance
   299  }
   300  
   301  func (r *notificationsReceiverMock) getDiscoveredInstances() []Instance {
   302  	r.discoveredInstancesMx.Lock()
   303  	defer r.discoveredInstancesMx.Unlock()
   304  
   305  	out := make([]Instance, 0, len(r.discoveredInstances))
   306  	for _, instance := range r.discoveredInstances {
   307  		out = append(out, instance)
   308  	}
   309  
   310  	sort.Slice(out, func(i, j int) bool {
   311  		return out[i].Address < out[j].Address
   312  	})
   313  
   314  	return out
   315  }